@tanstack/ai-gemini 0.13.0 → 0.14.1
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/dist/esm/adapters/audio.js +5 -1
- package/dist/esm/adapters/audio.js.map +1 -1
- package/dist/esm/adapters/image.js +6 -10
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/text.js +4 -6
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/adapters/tts.js +6 -2
- package/dist/esm/adapters/tts.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/usage.d.ts +67 -0
- package/dist/esm/usage.js +94 -0
- package/dist/esm/usage.js.map +1 -0
- package/package.json +3 -3
- package/src/adapters/audio.ts +6 -0
- package/src/adapters/image.ts +8 -10
- package/src/adapters/text.ts +5 -5
- package/src/adapters/tts.ts +10 -0
- package/src/index.ts +3 -0
- package/src/usage.ts +207 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseAudioAdapter } from "@tanstack/ai/adapters";
|
|
2
2
|
import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils/client.js";
|
|
3
|
+
import { buildGeminiUsage } from "../usage.js";
|
|
3
4
|
class GeminiAudioAdapter extends BaseAudioAdapter {
|
|
4
5
|
name = "gemini";
|
|
5
6
|
client;
|
|
@@ -36,7 +37,10 @@ class GeminiAudioAdapter extends BaseAudioAdapter {
|
|
|
36
37
|
audio: {
|
|
37
38
|
b64Json: audioPart.inlineData.data,
|
|
38
39
|
...contentType !== void 0 && { contentType }
|
|
39
|
-
}
|
|
40
|
+
},
|
|
41
|
+
// Surface token usage (with per-modality breakdown) when Gemini reports
|
|
42
|
+
// it. Spread conditionally for exactOptionalPropertyTypes.
|
|
43
|
+
...response.usageMetadata ? { usage: buildGeminiUsage(response.usageMetadata) } : {}
|
|
40
44
|
};
|
|
41
45
|
} catch (error) {
|
|
42
46
|
logger.errors("gemini.generateAudio fatal", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio.js","sources":["../../../src/adapters/audio.ts"],"sourcesContent":["import { BaseAudioAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport type { GEMINI_AUDIO_MODELS } from '../model-meta'\nimport type {\n AudioGenerationOptions,\n AudioGenerationResult,\n} from '@tanstack/ai'\nimport type { GoogleGenAI } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Provider options for Gemini Lyria music generation.\n *\n * Notes on the Lyria 3 surface area:\n * - `lyria-3-clip-preview` always returns MP3 (30-second clips). It does\n * not accept `responseMimeType`, and duration is fixed at 30 seconds —\n * the generic `duration` option on `AudioActivityOptions` is ignored.\n * - `lyria-3-pro-preview` returns MP3 by default. Duration is controlled\n * via the natural-language prompt, not a separate SDK field, so the\n * generic `duration` option is similarly ignored.\n * - `negativePrompt` is NOT accepted by `GenerateContentConfig` and has\n * therefore been removed from this surface to avoid giving callers a\n * silently-dropped knob.\n *\n * @see https://ai.google.dev/gemini-api/docs/music-generation\n */\nexport interface GeminiAudioProviderOptions {\n /**\n * Seed for deterministic generation.\n */\n seed?: number\n}\n\nexport interface GeminiAudioConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini Lyria audio generation */\nexport type GeminiAudioModel = (typeof GEMINI_AUDIO_MODELS)[number]\n\n/**\n * Gemini Lyria Music Generation Adapter.\n *\n * Tree-shakeable adapter for Google Lyria music generation via the Gemini API.\n *\n * Models:\n * - `lyria-3-pro-preview` — flagship model, full-length songs with verses,\n * choruses, and bridges. Outputs MP3 or WAV at 48 kHz stereo.\n * - `lyria-3-clip-preview` — 30-second clips in MP3.\n *\n * @see https://ai.google.dev/gemini-api/docs/music-generation\n *\n * @example\n * ```typescript\n * const adapter = geminiAudio('lyria-3-pro-preview')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'An upbeat jazz track with saxophone and drums',\n * })\n * ```\n */\nexport class GeminiAudioAdapter<\n TModel extends GeminiAudioModel,\n> extends BaseAudioAdapter<TModel, GeminiAudioProviderOptions> {\n readonly name = 'gemini' as const\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiAudioConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n async generateAudio(\n options: AudioGenerationOptions<GeminiAudioProviderOptions>,\n ): Promise<AudioGenerationResult> {\n const { model, prompt, modelOptions, logger } = options\n\n logger.request(`activity=generateAudio provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n try {\n // FIXME (SDK audit): Lyria 3 music generation may not belong on\n // generateContent at all — @google/genai exposes a `LiveMusicSession`\n // (`ai.live.music.connect`) with a `musicGenerationConfig` object.\n // `seed` is valid on GenerateContentConfig, and Lyria always returns\n // MP3 today, so we don't forward `responseMimeType` either.\n // The runtime test `emits only GenerateContentConfig-valid fields`\n // asserts the config shape so a later SDK audit can catch regressions.\n const response = await this.client.models.generateContent({\n model,\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\n config: {\n responseModalities: ['AUDIO', 'TEXT'],\n ...(modelOptions?.seed != null ? { seed: modelOptions.seed } : {}),\n },\n })\n\n const parts = response.candidates?.[0]?.content?.parts ?? []\n const audioPart = parts.find((part: any) =>\n part.inlineData?.mimeType?.startsWith('audio/'),\n )\n\n if (!audioPart?.inlineData?.data) {\n throw new Error('No audio data in Gemini Lyria response')\n }\n\n // audioPart was selected because mimeType.startsWith('audio/') was\n // truthy, so the mime type is guaranteed to be a string here. Trust the\n // value Gemini returned rather than inventing a non-standard\n // `audio/mp3` fallback (IANA is `audio/mpeg`).\n const contentType = audioPart.inlineData.mimeType\n\n return {\n id: generateId(this.name),\n model,\n audio: {\n b64Json: audioPart.inlineData.data,\n ...(contentType !== undefined && { contentType }),\n },\n }\n } catch (error) {\n logger.errors('gemini.generateAudio fatal', {\n error,\n source: 'gemini.generateAudio',\n })\n throw error\n }\n }\n}\n\n/**\n * Creates a Gemini Lyria audio adapter with an explicit API key.\n *\n * @param model - The Lyria model name (e.g., 'lyria-3-pro-preview')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n *\n * @example\n * ```typescript\n * const adapter = createGeminiAudio('lyria-3-pro-preview', 'your-api-key')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'Ambient electronic music with soft pads',\n * })\n * ```\n */\nexport function createGeminiAudio<TModel extends GeminiAudioModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiAudioConfig, 'apiKey'>,\n): GeminiAudioAdapter<TModel> {\n // Put apiKey LAST so caller-supplied config can't silently override the\n // explicit argument.\n return new GeminiAudioAdapter({ ...config, apiKey }, model)\n}\n\n/**\n * Creates a Gemini Lyria audio adapter with automatic API key detection.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in the environment.\n *\n * @param model - The Lyria model name (e.g., 'lyria-3-pro-preview')\n * @param config - Optional configuration (excluding apiKey)\n *\n * @example\n * ```typescript\n * const adapter = geminiAudio('lyria-3-pro-preview')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'An orchestral piece with strings and brass',\n * })\n * ```\n */\nexport function geminiAudio<TModel extends GeminiAudioModel>(\n model: TModel,\n config?: Omit<GeminiAudioConfig, 'apiKey'>,\n): GeminiAudioAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiAudio(model, apiKey, config)\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"audio.js","sources":["../../../src/adapters/audio.ts"],"sourcesContent":["import { BaseAudioAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { buildGeminiUsage } from '../usage'\nimport type { GEMINI_AUDIO_MODELS } from '../model-meta'\nimport type {\n AudioGenerationOptions,\n AudioGenerationResult,\n} from '@tanstack/ai'\nimport type { GoogleGenAI } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Provider options for Gemini Lyria music generation.\n *\n * Notes on the Lyria 3 surface area:\n * - `lyria-3-clip-preview` always returns MP3 (30-second clips). It does\n * not accept `responseMimeType`, and duration is fixed at 30 seconds —\n * the generic `duration` option on `AudioActivityOptions` is ignored.\n * - `lyria-3-pro-preview` returns MP3 by default. Duration is controlled\n * via the natural-language prompt, not a separate SDK field, so the\n * generic `duration` option is similarly ignored.\n * - `negativePrompt` is NOT accepted by `GenerateContentConfig` and has\n * therefore been removed from this surface to avoid giving callers a\n * silently-dropped knob.\n *\n * @see https://ai.google.dev/gemini-api/docs/music-generation\n */\nexport interface GeminiAudioProviderOptions {\n /**\n * Seed for deterministic generation.\n */\n seed?: number\n}\n\nexport interface GeminiAudioConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini Lyria audio generation */\nexport type GeminiAudioModel = (typeof GEMINI_AUDIO_MODELS)[number]\n\n/**\n * Gemini Lyria Music Generation Adapter.\n *\n * Tree-shakeable adapter for Google Lyria music generation via the Gemini API.\n *\n * Models:\n * - `lyria-3-pro-preview` — flagship model, full-length songs with verses,\n * choruses, and bridges. Outputs MP3 or WAV at 48 kHz stereo.\n * - `lyria-3-clip-preview` — 30-second clips in MP3.\n *\n * @see https://ai.google.dev/gemini-api/docs/music-generation\n *\n * @example\n * ```typescript\n * const adapter = geminiAudio('lyria-3-pro-preview')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'An upbeat jazz track with saxophone and drums',\n * })\n * ```\n */\nexport class GeminiAudioAdapter<\n TModel extends GeminiAudioModel,\n> extends BaseAudioAdapter<TModel, GeminiAudioProviderOptions> {\n readonly name = 'gemini' as const\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiAudioConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n async generateAudio(\n options: AudioGenerationOptions<GeminiAudioProviderOptions>,\n ): Promise<AudioGenerationResult> {\n const { model, prompt, modelOptions, logger } = options\n\n logger.request(`activity=generateAudio provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n try {\n // FIXME (SDK audit): Lyria 3 music generation may not belong on\n // generateContent at all — @google/genai exposes a `LiveMusicSession`\n // (`ai.live.music.connect`) with a `musicGenerationConfig` object.\n // `seed` is valid on GenerateContentConfig, and Lyria always returns\n // MP3 today, so we don't forward `responseMimeType` either.\n // The runtime test `emits only GenerateContentConfig-valid fields`\n // asserts the config shape so a later SDK audit can catch regressions.\n const response = await this.client.models.generateContent({\n model,\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\n config: {\n responseModalities: ['AUDIO', 'TEXT'],\n ...(modelOptions?.seed != null ? { seed: modelOptions.seed } : {}),\n },\n })\n\n const parts = response.candidates?.[0]?.content?.parts ?? []\n const audioPart = parts.find((part: any) =>\n part.inlineData?.mimeType?.startsWith('audio/'),\n )\n\n if (!audioPart?.inlineData?.data) {\n throw new Error('No audio data in Gemini Lyria response')\n }\n\n // audioPart was selected because mimeType.startsWith('audio/') was\n // truthy, so the mime type is guaranteed to be a string here. Trust the\n // value Gemini returned rather than inventing a non-standard\n // `audio/mp3` fallback (IANA is `audio/mpeg`).\n const contentType = audioPart.inlineData.mimeType\n\n return {\n id: generateId(this.name),\n model,\n audio: {\n b64Json: audioPart.inlineData.data,\n ...(contentType !== undefined && { contentType }),\n },\n // Surface token usage (with per-modality breakdown) when Gemini reports\n // it. Spread conditionally for exactOptionalPropertyTypes.\n ...(response.usageMetadata\n ? { usage: buildGeminiUsage(response.usageMetadata) }\n : {}),\n }\n } catch (error) {\n logger.errors('gemini.generateAudio fatal', {\n error,\n source: 'gemini.generateAudio',\n })\n throw error\n }\n }\n}\n\n/**\n * Creates a Gemini Lyria audio adapter with an explicit API key.\n *\n * @param model - The Lyria model name (e.g., 'lyria-3-pro-preview')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n *\n * @example\n * ```typescript\n * const adapter = createGeminiAudio('lyria-3-pro-preview', 'your-api-key')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'Ambient electronic music with soft pads',\n * })\n * ```\n */\nexport function createGeminiAudio<TModel extends GeminiAudioModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiAudioConfig, 'apiKey'>,\n): GeminiAudioAdapter<TModel> {\n // Put apiKey LAST so caller-supplied config can't silently override the\n // explicit argument.\n return new GeminiAudioAdapter({ ...config, apiKey }, model)\n}\n\n/**\n * Creates a Gemini Lyria audio adapter with automatic API key detection.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in the environment.\n *\n * @param model - The Lyria model name (e.g., 'lyria-3-pro-preview')\n * @param config - Optional configuration (excluding apiKey)\n *\n * @example\n * ```typescript\n * const adapter = geminiAudio('lyria-3-pro-preview')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'An orchestral piece with strings and brass',\n * })\n * ```\n */\nexport function geminiAudio<TModel extends GeminiAudioModel>(\n model: TModel,\n config?: Omit<GeminiAudioConfig, 'apiKey'>,\n): GeminiAudioAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiAudio(model, apiKey, config)\n}\n"],"names":[],"mappings":";;;AAgEO,MAAM,2BAEH,iBAAqD;AAAA,EACpD,OAAO;AAAA,EAEC;AAAA,EAEjB,YAAY,QAA2B,OAAe;AACpD,UAAM,OAAO,MAAM;AACnB,SAAK,SAAS,mBAAmB,MAAM;AAAA,EACzC;AAAA,EAEA,MAAM,cACJ,SACgC;AAChC,UAAM,EAAE,OAAO,QAAQ,cAAc,WAAW;AAEhD,WAAO,QAAQ,gDAAgD,KAAK,IAAI;AAAA,MACtE,UAAU;AAAA,MACV;AAAA,IAAA,CACD;AAED,QAAI;AAQF,YAAM,WAAW,MAAM,KAAK,OAAO,OAAO,gBAAgB;AAAA,QACxD;AAAA,QACA,UAAU,CAAC,EAAE,MAAM,QAAQ,OAAO,CAAC,EAAE,MAAM,OAAA,CAAQ,GAAG;AAAA,QACtD,QAAQ;AAAA,UACN,oBAAoB,CAAC,SAAS,MAAM;AAAA,UACpC,GAAI,cAAc,QAAQ,OAAO,EAAE,MAAM,aAAa,SAAS,CAAA;AAAA,QAAC;AAAA,MAClE,CACD;AAED,YAAM,QAAQ,SAAS,aAAa,CAAC,GAAG,SAAS,SAAS,CAAA;AAC1D,YAAM,YAAY,MAAM;AAAA,QAAK,CAAC,SAC5B,KAAK,YAAY,UAAU,WAAW,QAAQ;AAAA,MAAA;AAGhD,UAAI,CAAC,WAAW,YAAY,MAAM;AAChC,cAAM,IAAI,MAAM,wCAAwC;AAAA,MAC1D;AAMA,YAAM,cAAc,UAAU,WAAW;AAEzC,aAAO;AAAA,QACL,IAAI,WAAW,KAAK,IAAI;AAAA,QACxB;AAAA,QACA,OAAO;AAAA,UACL,SAAS,UAAU,WAAW;AAAA,UAC9B,GAAI,gBAAgB,UAAa,EAAE,YAAA;AAAA,QAAY;AAAA;AAAA;AAAA,QAIjD,GAAI,SAAS,gBACT,EAAE,OAAO,iBAAiB,SAAS,aAAa,MAChD,CAAA;AAAA,MAAC;AAAA,IAET,SAAS,OAAO;AACd,aAAO,OAAO,8BAA8B;AAAA,QAC1C;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAkBO,SAAS,kBACd,OACA,QACA,QAC4B;AAG5B,SAAO,IAAI,mBAAmB,EAAE,GAAG,QAAQ,OAAA,GAAU,KAAK;AAC5D;AAmBO,SAAS,YACd,OACA,QAC4B;AAC5B,QAAM,SAAS,uBAAA;AACf,SAAO,kBAAkB,OAAO,QAAQ,MAAM;AAChD;"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseImageAdapter } from "@tanstack/ai/adapters";
|
|
2
2
|
import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils/client.js";
|
|
3
|
+
import { buildGeminiUsage } from "../usage.js";
|
|
3
4
|
import { validatePrompt, validateImageSize, validateNumberOfImages, parseNativeImageSize, sizeToAspectRatio } from "../image/image-provider-options.js";
|
|
4
5
|
class GeminiImageAdapter extends BaseImageAdapter {
|
|
5
6
|
kind = "image";
|
|
@@ -94,16 +95,11 @@ class GeminiImageAdapter extends BaseImageAdapter {
|
|
|
94
95
|
id: generateId(this.name),
|
|
95
96
|
model,
|
|
96
97
|
images,
|
|
97
|
-
// Surface token usage when the model
|
|
98
|
-
// generateContent). Conditionally spread
|
|
99
|
-
// exactOptionalPropertyTypes — only include usage when
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
inputTokens: response.usageMetadata.promptTokenCount ?? 0,
|
|
103
|
-
outputTokens: response.usageMetadata.candidatesTokenCount ?? 0,
|
|
104
|
-
totalTokens: response.usageMetadata.totalTokenCount ?? 0
|
|
105
|
-
}
|
|
106
|
-
}
|
|
98
|
+
// Surface token usage (with per-modality breakdown) when the model
|
|
99
|
+
// reports it (e.g. Nano Banana via generateContent). Conditionally spread
|
|
100
|
+
// to satisfy exactOptionalPropertyTypes — only include usage when
|
|
101
|
+
// present. See #330.
|
|
102
|
+
...response.usageMetadata ? { usage: buildGeminiUsage(response.usageMetadata) } : {}
|
|
107
103
|
};
|
|
108
104
|
}
|
|
109
105
|
buildImagenConfig(options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport {\n parseNativeImageSize,\n sizeToAspectRatio,\n validateImageSize,\n validateNumberOfImages,\n validatePrompt,\n} from '../image/image-provider-options'\nimport type { GEMINI_IMAGE_MODELS } from '../model-meta'\nimport type {\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName,\n GeminiImageProviderOptions,\n} from '../image/image-provider-options'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n} from '@tanstack/ai'\nimport type {\n GenerateContentConfig,\n GenerateContentResponse,\n GenerateImagesConfig,\n GenerateImagesResponse,\n GoogleGenAI,\n} from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for Gemini image adapter\n */\nexport interface GeminiImageConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini Image */\nexport type GeminiImageModel = (typeof GEMINI_IMAGE_MODELS)[number]\n\n/**\n * Gemini Image Generation Adapter\n *\n * Tree-shakeable adapter for Gemini image generation functionality.\n * Supports Imagen 3/4 models (via generateImages API) and Gemini native\n * image models like Nano Banana 2 (via generateContent API).\n *\n * Features:\n * - Aspect ratio-based image sizing\n * - Person generation controls\n * - Safety filtering\n * - Watermark options\n * - Extended resolution tiers (Nano Banana 2)\n */\nexport class GeminiImageAdapter<\n TModel extends GeminiImageModel,\n> extends BaseImageAdapter<\n TModel,\n GeminiImageProviderOptions,\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName\n> {\n override readonly kind = 'image' as const\n readonly name = 'gemini' as const\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: GeminiImageProviderOptions\n modelProviderOptionsByName: GeminiImageModelProviderOptionsByName\n modelSizeByName: GeminiImageModelSizeByName\n }\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiImageConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n async generateImages(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, prompt, logger } = options\n\n logger.request(\n `activity=generateImage provider=gemini model=${this.model}`,\n {\n provider: 'gemini',\n model: this.model,\n },\n )\n\n try {\n validatePrompt({ prompt, model })\n\n if (this.isGeminiImageModel(model)) {\n return await this.generateWithGeminiApi(options)\n }\n\n // Imagen models path (generateImages API)\n validateImageSize(model, options.size)\n validateNumberOfImages(model, options.numberOfImages)\n\n const config = this.buildImagenConfig(options)\n\n const response = await this.client.models.generateImages({\n model,\n prompt,\n config,\n })\n\n return this.transformImagenResponse(model, response)\n } catch (error) {\n logger.errors('gemini.generateImage fatal', {\n error,\n source: 'gemini.generateImage',\n })\n throw error\n }\n }\n\n private isGeminiImageModel(model: string): boolean {\n return model.startsWith('gemini-')\n }\n\n private async generateWithGeminiApi(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, prompt, size, numberOfImages, modelOptions } = options\n\n const parsedSize = size ? parseNativeImageSize(size) : undefined\n\n // The generateContent API has no numberOfImages parameter.\n // Instead, augment the prompt to request multiple images when needed.\n const augmentedPrompt =\n numberOfImages && numberOfImages > 1\n ? `${prompt} Generate ${numberOfImages} distinct images.`\n : prompt\n\n // GeminiImageProviderOptions is Imagen-shaped — most fields\n // (personGeneration, safetyFilterLevel, addWatermark, outputMimeType,\n // outputCompressionQuality, guidanceScale, enhancePrompt,\n // includeSafetyAttributes, includeRaiReason, outputGcsUri, labels,\n // negativePrompt, language) are only valid on GenerateImagesConfig and\n // would be rejected by the Gemini-native generateContent path. Pick only\n // the fields that are valid on GenerateContentConfig instead of spreading\n // the whole options object.\n const nativeConfig: GenerateContentConfig = {}\n if (modelOptions?.seed !== undefined) {\n nativeConfig.seed = modelOptions.seed\n }\n\n const config: GenerateContentConfig = {\n ...nativeConfig,\n // Include TEXT so the model can interleave descriptions between images.\n // IMPORTANT: responseModalities is a protected default — set it AFTER\n // nativeConfig so nothing can silently disable image output.\n responseModalities: ['TEXT', 'IMAGE'],\n ...(parsedSize && {\n imageConfig: {\n ...(parsedSize.aspectRatio && {\n aspectRatio: parsedSize.aspectRatio,\n }),\n ...(parsedSize.resolution && {\n imageSize: parsedSize.resolution,\n }),\n },\n }),\n }\n\n const response = await this.client.models.generateContent({\n model,\n contents: augmentedPrompt,\n config,\n })\n\n return this.transformGeminiResponse(model, response)\n }\n\n private transformGeminiResponse(\n model: string,\n response: GenerateContentResponse,\n ): ImageGenerationResult {\n const images: Array<GeneratedImage> = []\n const textParts: Array<string> = []\n const parts = response.candidates?.[0]?.content?.parts ?? []\n\n for (const part of parts) {\n if (\n part.inlineData?.data &&\n typeof part.inlineData.data === 'string' &&\n part.inlineData.data.length > 0\n ) {\n images.push({ b64Json: part.inlineData.data })\n } else if (typeof part.text === 'string' && part.text.length > 0) {\n textParts.push(part.text)\n }\n }\n\n // If the model returned only text parts (for example a safety refusal\n // or a \"can't do that\" message), surface the text instead of silently\n // resolving to an empty images array — otherwise callers can't tell a\n // generation failure apart from a genuine empty response.\n if (images.length === 0) {\n const reason =\n textParts.length > 0\n ? `: ${textParts.join(' ').trim()}`\n : ' (no inline image or text parts were returned).'\n throw new Error(`Gemini ${model} returned no images${reason}`)\n }\n\n return {\n id: generateId(this.name),\n model,\n images,\n // Surface token usage when the model reports it (e.g. Nano Banana via\n // generateContent). Conditionally spread to satisfy\n // exactOptionalPropertyTypes — only include usage when present. See #330.\n ...(response.usageMetadata && {\n usage: {\n inputTokens: response.usageMetadata.promptTokenCount ?? 0,\n outputTokens: response.usageMetadata.candidatesTokenCount ?? 0,\n totalTokens: response.usageMetadata.totalTokenCount ?? 0,\n },\n }),\n }\n }\n\n private buildImagenConfig(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): GenerateImagesConfig {\n const { size, numberOfImages, modelOptions } = options\n\n // Build with conditional spreads — under exactOptionalPropertyTypes the\n // vendor `GenerateImagesConfig` fields are `field?: T` (no `| undefined`),\n // so we can only assign the property when we actually have a value.\n const sizeAspectRatio = size ? sizeToAspectRatio(size) : undefined\n return {\n numberOfImages: numberOfImages ?? 1,\n // Map size to aspect ratio if provided (modelOptions.aspectRatio will override)\n ...(sizeAspectRatio !== undefined && { aspectRatio: sizeAspectRatio }),\n ...modelOptions,\n }\n }\n\n private transformImagenResponse(\n model: string,\n response: GenerateImagesResponse,\n ): ImageGenerationResult {\n const entries = response.generatedImages ?? []\n const images: Array<GeneratedImage> = []\n const filterReasons: Array<string> = []\n\n for (const item of entries) {\n const b64Json = item.image?.imageBytes\n if (b64Json) {\n images.push({\n b64Json,\n ...(item.enhancedPrompt !== undefined && {\n revisedPrompt: item.enhancedPrompt,\n }),\n })\n continue\n }\n // Imagen can drop individual entries with a raiFilteredReason when\n // Responsible-AI filters fire. Preserve the reason so callers can\n // surface it instead of silently getting back fewer images.\n const reason = (item as { raiFilteredReason?: string }).raiFilteredReason\n if (reason) {\n filterReasons.push(reason)\n }\n }\n\n // Every entry was filtered — no usable images to return. Throw rather\n // than resolve to an empty array so the caller is forced to handle the\n // failure mode explicitly.\n if (entries.length > 0 && images.length === 0) {\n const joined = filterReasons.length > 0 ? filterReasons.join('; ') : ''\n throw new Error(\n `Imagen ${model} returned no images: all ${entries.length} generated image(s) were filtered by Responsible-AI${joined ? ` (${joined})` : ''}.`,\n )\n }\n\n // Partial filter: surface via console.warn since ImageGenerationResult\n // has no warnings field. Callers that care can still inspect the count\n // mismatch between requested and returned images.\n if (filterReasons.length > 0 && typeof console !== 'undefined') {\n console.warn(\n `[gemini-image] ${filterReasons.length} of ${entries.length} images from ${model} were filtered by Responsible-AI: ${filterReasons.join('; ')}`,\n )\n }\n\n return {\n id: generateId(this.name),\n model,\n images,\n }\n }\n}\n\n/**\n * Creates a Gemini image adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'imagen-3.0-generate-002')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini image adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiImage('imagen-3.0-generate-002', \"your-api-key\");\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A cute baby sea otter'\n * });\n * ```\n */\nexport function createGeminiImage<TModel extends GeminiImageModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<TModel> {\n return new GeminiImageAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini image adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'imagen-4.0-generate-001')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini image adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiImage('imagen-4.0-generate-001');\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A beautiful sunset over mountains'\n * });\n * ```\n */\nexport function geminiImage<TModel extends GeminiImageModel>(\n model: TModel,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiImage(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"image.js","sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { buildGeminiUsage } from '../usage'\nimport {\n parseNativeImageSize,\n sizeToAspectRatio,\n validateImageSize,\n validateNumberOfImages,\n validatePrompt,\n} from '../image/image-provider-options'\nimport type { GEMINI_IMAGE_MODELS } from '../model-meta'\nimport type {\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName,\n GeminiImageProviderOptions,\n} from '../image/image-provider-options'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n} from '@tanstack/ai'\nimport type {\n GenerateContentConfig,\n GenerateContentResponse,\n GenerateImagesConfig,\n GenerateImagesResponse,\n GoogleGenAI,\n} from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for Gemini image adapter\n */\nexport interface GeminiImageConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini Image */\nexport type GeminiImageModel = (typeof GEMINI_IMAGE_MODELS)[number]\n\n/**\n * Gemini Image Generation Adapter\n *\n * Tree-shakeable adapter for Gemini image generation functionality.\n * Supports Imagen 3/4 models (via generateImages API) and Gemini native\n * image models like Nano Banana 2 (via generateContent API).\n *\n * Features:\n * - Aspect ratio-based image sizing\n * - Person generation controls\n * - Safety filtering\n * - Watermark options\n * - Extended resolution tiers (Nano Banana 2)\n */\nexport class GeminiImageAdapter<\n TModel extends GeminiImageModel,\n> extends BaseImageAdapter<\n TModel,\n GeminiImageProviderOptions,\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName\n> {\n override readonly kind = 'image' as const\n readonly name = 'gemini' as const\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: GeminiImageProviderOptions\n modelProviderOptionsByName: GeminiImageModelProviderOptionsByName\n modelSizeByName: GeminiImageModelSizeByName\n }\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiImageConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n async generateImages(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, prompt, logger } = options\n\n logger.request(\n `activity=generateImage provider=gemini model=${this.model}`,\n {\n provider: 'gemini',\n model: this.model,\n },\n )\n\n try {\n validatePrompt({ prompt, model })\n\n if (this.isGeminiImageModel(model)) {\n return await this.generateWithGeminiApi(options)\n }\n\n // Imagen models path (generateImages API)\n validateImageSize(model, options.size)\n validateNumberOfImages(model, options.numberOfImages)\n\n const config = this.buildImagenConfig(options)\n\n const response = await this.client.models.generateImages({\n model,\n prompt,\n config,\n })\n\n return this.transformImagenResponse(model, response)\n } catch (error) {\n logger.errors('gemini.generateImage fatal', {\n error,\n source: 'gemini.generateImage',\n })\n throw error\n }\n }\n\n private isGeminiImageModel(model: string): boolean {\n return model.startsWith('gemini-')\n }\n\n private async generateWithGeminiApi(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, prompt, size, numberOfImages, modelOptions } = options\n\n const parsedSize = size ? parseNativeImageSize(size) : undefined\n\n // The generateContent API has no numberOfImages parameter.\n // Instead, augment the prompt to request multiple images when needed.\n const augmentedPrompt =\n numberOfImages && numberOfImages > 1\n ? `${prompt} Generate ${numberOfImages} distinct images.`\n : prompt\n\n // GeminiImageProviderOptions is Imagen-shaped — most fields\n // (personGeneration, safetyFilterLevel, addWatermark, outputMimeType,\n // outputCompressionQuality, guidanceScale, enhancePrompt,\n // includeSafetyAttributes, includeRaiReason, outputGcsUri, labels,\n // negativePrompt, language) are only valid on GenerateImagesConfig and\n // would be rejected by the Gemini-native generateContent path. Pick only\n // the fields that are valid on GenerateContentConfig instead of spreading\n // the whole options object.\n const nativeConfig: GenerateContentConfig = {}\n if (modelOptions?.seed !== undefined) {\n nativeConfig.seed = modelOptions.seed\n }\n\n const config: GenerateContentConfig = {\n ...nativeConfig,\n // Include TEXT so the model can interleave descriptions between images.\n // IMPORTANT: responseModalities is a protected default — set it AFTER\n // nativeConfig so nothing can silently disable image output.\n responseModalities: ['TEXT', 'IMAGE'],\n ...(parsedSize && {\n imageConfig: {\n ...(parsedSize.aspectRatio && {\n aspectRatio: parsedSize.aspectRatio,\n }),\n ...(parsedSize.resolution && {\n imageSize: parsedSize.resolution,\n }),\n },\n }),\n }\n\n const response = await this.client.models.generateContent({\n model,\n contents: augmentedPrompt,\n config,\n })\n\n return this.transformGeminiResponse(model, response)\n }\n\n private transformGeminiResponse(\n model: string,\n response: GenerateContentResponse,\n ): ImageGenerationResult {\n const images: Array<GeneratedImage> = []\n const textParts: Array<string> = []\n const parts = response.candidates?.[0]?.content?.parts ?? []\n\n for (const part of parts) {\n if (\n part.inlineData?.data &&\n typeof part.inlineData.data === 'string' &&\n part.inlineData.data.length > 0\n ) {\n images.push({ b64Json: part.inlineData.data })\n } else if (typeof part.text === 'string' && part.text.length > 0) {\n textParts.push(part.text)\n }\n }\n\n // If the model returned only text parts (for example a safety refusal\n // or a \"can't do that\" message), surface the text instead of silently\n // resolving to an empty images array — otherwise callers can't tell a\n // generation failure apart from a genuine empty response.\n if (images.length === 0) {\n const reason =\n textParts.length > 0\n ? `: ${textParts.join(' ').trim()}`\n : ' (no inline image or text parts were returned).'\n throw new Error(`Gemini ${model} returned no images${reason}`)\n }\n\n return {\n id: generateId(this.name),\n model,\n images,\n // Surface token usage (with per-modality breakdown) when the model\n // reports it (e.g. Nano Banana via generateContent). Conditionally spread\n // to satisfy exactOptionalPropertyTypes — only include usage when\n // present. See #330.\n ...(response.usageMetadata\n ? { usage: buildGeminiUsage(response.usageMetadata) }\n : {}),\n }\n }\n\n private buildImagenConfig(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): GenerateImagesConfig {\n const { size, numberOfImages, modelOptions } = options\n\n // Build with conditional spreads — under exactOptionalPropertyTypes the\n // vendor `GenerateImagesConfig` fields are `field?: T` (no `| undefined`),\n // so we can only assign the property when we actually have a value.\n const sizeAspectRatio = size ? sizeToAspectRatio(size) : undefined\n return {\n numberOfImages: numberOfImages ?? 1,\n // Map size to aspect ratio if provided (modelOptions.aspectRatio will override)\n ...(sizeAspectRatio !== undefined && { aspectRatio: sizeAspectRatio }),\n ...modelOptions,\n }\n }\n\n private transformImagenResponse(\n model: string,\n response: GenerateImagesResponse,\n ): ImageGenerationResult {\n const entries = response.generatedImages ?? []\n const images: Array<GeneratedImage> = []\n const filterReasons: Array<string> = []\n\n for (const item of entries) {\n const b64Json = item.image?.imageBytes\n if (b64Json) {\n images.push({\n b64Json,\n ...(item.enhancedPrompt !== undefined && {\n revisedPrompt: item.enhancedPrompt,\n }),\n })\n continue\n }\n // Imagen can drop individual entries with a raiFilteredReason when\n // Responsible-AI filters fire. Preserve the reason so callers can\n // surface it instead of silently getting back fewer images.\n const reason = (item as { raiFilteredReason?: string }).raiFilteredReason\n if (reason) {\n filterReasons.push(reason)\n }\n }\n\n // Every entry was filtered — no usable images to return. Throw rather\n // than resolve to an empty array so the caller is forced to handle the\n // failure mode explicitly.\n if (entries.length > 0 && images.length === 0) {\n const joined = filterReasons.length > 0 ? filterReasons.join('; ') : ''\n throw new Error(\n `Imagen ${model} returned no images: all ${entries.length} generated image(s) were filtered by Responsible-AI${joined ? ` (${joined})` : ''}.`,\n )\n }\n\n // Partial filter: surface via console.warn since ImageGenerationResult\n // has no warnings field. Callers that care can still inspect the count\n // mismatch between requested and returned images.\n if (filterReasons.length > 0 && typeof console !== 'undefined') {\n console.warn(\n `[gemini-image] ${filterReasons.length} of ${entries.length} images from ${model} were filtered by Responsible-AI: ${filterReasons.join('; ')}`,\n )\n }\n\n return {\n id: generateId(this.name),\n model,\n images,\n }\n }\n}\n\n/**\n * Creates a Gemini image adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'imagen-3.0-generate-002')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini image adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiImage('imagen-3.0-generate-002', \"your-api-key\");\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A cute baby sea otter'\n * });\n * ```\n */\nexport function createGeminiImage<TModel extends GeminiImageModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<TModel> {\n return new GeminiImageAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini image adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'imagen-4.0-generate-001')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini image adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiImage('imagen-4.0-generate-001');\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A beautiful sunset over mountains'\n * });\n * ```\n */\nexport function geminiImage<TModel extends GeminiImageModel>(\n model: TModel,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiImage(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
|
|
|
3
3
|
import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
|
|
4
4
|
import { BaseTextAdapter } from "@tanstack/ai/adapters";
|
|
5
5
|
import { convertToolsToProviderFormat } from "../tools/tool-converter.js";
|
|
6
|
+
import { buildGeminiUsage } from "../usage.js";
|
|
6
7
|
import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils/client.js";
|
|
7
8
|
import { GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS } from "../model-meta.js";
|
|
8
9
|
class GeminiTextAdapter extends BaseTextAdapter {
|
|
@@ -76,7 +77,8 @@ class GeminiTextAdapter extends BaseTextAdapter {
|
|
|
76
77
|
}
|
|
77
78
|
return {
|
|
78
79
|
data: parsed,
|
|
79
|
-
rawText
|
|
80
|
+
rawText,
|
|
81
|
+
usage: result.usageMetadata ? buildGeminiUsage(result.usageMetadata) : void 0
|
|
80
82
|
};
|
|
81
83
|
} catch (error) {
|
|
82
84
|
logger.errors("gemini.structuredOutput fatal", {
|
|
@@ -410,11 +412,7 @@ class GeminiTextAdapter extends BaseTextAdapter {
|
|
|
410
412
|
// exactOptionalPropertyTypes; only include it when usageMetadata is
|
|
411
413
|
// present rather than assigning an explicit `undefined`.
|
|
412
414
|
...chunk.usageMetadata && {
|
|
413
|
-
usage:
|
|
414
|
-
promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,
|
|
415
|
-
completionTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
|
|
416
|
-
totalTokens: chunk.usageMetadata.totalTokenCount ?? 0
|
|
417
|
-
}
|
|
415
|
+
usage: buildGeminiUsage(chunk.usageMetadata)
|
|
418
416
|
}
|
|
419
417
|
};
|
|
420
418
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { convertToolsToProviderFormat } from '../tools/tool-converter'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS } from '../model-meta'\nimport type {\n GEMINI_MODELS,\n GeminiChatModelProviderOptionsByName,\n GeminiChatModelToolCapabilitiesByName,\n GeminiModelInputModalitiesByName,\n} from '../model-meta'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type {\n Content,\n GenerateContentParameters,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n ThinkingLevel,\n} from '@google/genai'\nimport type {\n ContentPart,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { ExternalTextProviderOptions } from '../text/text-provider-options'\nimport type {\n GeminiMessageMetadataByModality,\n GeminiToolCallMetadata,\n} from '../message-types'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for Gemini text adapter\n */\nexport interface GeminiTextConfig extends GeminiClientConfig {}\n\n/**\n * Gemini-specific provider options for text/chat\n */\nexport type GeminiTextProviderOptions = ExternalTextProviderOptions\n\n// ===========================\n// Type Resolution Helpers\n// ===========================\n\n/**\n * Resolve provider options for a specific model.\n * If the model has explicit options in the map, use those; otherwise use base options.\n */\ntype ResolveProviderOptions<TModel extends string> =\n TModel extends keyof GeminiChatModelProviderOptionsByName\n ? GeminiChatModelProviderOptionsByName[TModel]\n : GeminiTextProviderOptions\n\n/**\n * Resolve input modalities for a specific model.\n * If the model has explicit modalities in the map, use those; otherwise use all modalities.\n */\ntype ResolveInputModalities<TModel extends string> =\n TModel extends keyof GeminiModelInputModalitiesByName\n ? GeminiModelInputModalitiesByName[TModel]\n : readonly ['text', 'image', 'audio', 'video', 'document']\n\n/**\n * Resolve tool capabilities for a specific model.\n * If the model has explicit tools in the map, use those; otherwise use empty tuple.\n */\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof GeminiChatModelToolCapabilitiesByName\n ? NonNullable<GeminiChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n// ===========================\n// Adapter Implementation\n// ===========================\n\n/**\n * Gemini Text (Chat) Adapter\n *\n * Tree-shakeable adapter for Gemini chat/text completion functionality.\n * Import only what you need for smaller bundle sizes.\n */\nexport class GeminiTextAdapter<\n TModel extends (typeof GEMINI_MODELS)[number],\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GeminiMessageMetadataByModality,\n TToolCapabilities,\n GeminiToolCallMetadata\n> {\n override readonly kind = 'text' as const\n readonly name = 'gemini' as const\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiTextConfig, model: TModel) {\n super({}, model)\n this.client = createGeminiClient(config)\n }\n\n async *chatStream(\n options: TextOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const mappedOptions = this.mapCommonOptionsToGemini(options)\n const { logger } = options\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,\n { provider: 'gemini', model: this.model },\n )\n const result =\n await this.client.models.generateContentStream(mappedOptions)\n\n yield* this.processStreamChunks(result, options, logger)\n } catch (error) {\n const rawEvent = toRunErrorRawEvent(error)\n logger.errors('gemini.chatStream fatal', {\n error,\n source: 'gemini.chatStream',\n })\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n // Forward the provider's structured error body when present (see\n // toRunErrorRawEvent); omitted otherwise.\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n },\n }\n }\n }\n\n /**\n * Generate structured output using Gemini's native JSON response format.\n * Uses responseMimeType: 'application/json' and responseSchema for structured output.\n * The outputSchema is already JSON Schema (converted in the ai layer).\n */\n async structuredOutput(\n options: StructuredOutputOptions<GeminiTextProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const { chatOptions, outputSchema } = options\n const { logger } = chatOptions\n\n const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${this.model} messages=${chatOptions.messages.length} tools=${chatOptions.tools?.length ?? 0} stream=false`,\n { provider: 'gemini', model: this.model },\n )\n // Add structured output configuration\n const result = await this.client.models.generateContent({\n ...mappedOptions,\n config: {\n ...mappedOptions.config,\n responseMimeType: 'application/json',\n responseSchema: outputSchema,\n },\n })\n\n // Extract text content from the response\n const rawText = this.extractTextFromResponse(result)\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n return {\n data: parsed,\n rawText,\n }\n } catch (error) {\n logger.errors('gemini.structuredOutput fatal', {\n error,\n source: 'gemini.structuredOutput',\n })\n throw new Error(\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during structured output generation.',\n )\n }\n }\n\n /**\n * Extract text content from a non-streaming response\n */\n private extractTextFromResponse(response: GenerateContentResponse): string {\n let textContent = ''\n\n if (response.candidates?.[0]?.content?.parts) {\n for (const part of response.candidates[0].content.parts) {\n if (part.text) {\n textContent += part.text\n }\n }\n }\n\n return textContent\n }\n\n private async *processStreamChunks(\n result: AsyncGenerator<GenerateContentResponse, unknown, unknown>,\n options: TextOptions<GeminiTextProviderOptions>,\n logger: InternalLogger,\n ): AsyncIterable<StreamChunk> {\n const model = options.model\n let accumulatedContent = ''\n let accumulatedThinking = ''\n const toolCallMap = new Map<\n string,\n {\n name: string\n args: string\n index: number\n started: boolean\n thoughtSignature?: string\n }\n >()\n let nextToolIndex = 0\n\n // AG-UI lifecycle tracking\n const runId = options.runId ?? generateId(this.name)\n const threadId = options.threadId ?? generateId(this.name)\n const messageId = generateId(this.name)\n let stepId: string | null = null\n let reasoningMessageId: string | null = null\n let hasClosedReasoning = false\n let hasEmittedRunStarted = false\n let hasEmittedTextMessageStart = false\n let hasEmittedStepStarted = false\n\n for await (const chunk of result) {\n logger.provider(`provider=gemini`, { chunk })\n // Emit RUN_STARTED on first chunk\n if (!hasEmittedRunStarted) {\n hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\n }\n }\n\n if (chunk.candidates?.[0]?.content?.parts) {\n const parts = chunk.candidates[0].content.parts\n\n for (const part of parts) {\n if (part.text) {\n if (part.thought) {\n // Emit STEP_STARTED and REASONING events on first thinking content\n if (!hasEmittedStepStarted) {\n hasEmittedStepStarted = true\n stepId = generateId(this.name)\n reasoningMessageId = generateId(this.name)\n\n // Spec REASONING events\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model,\n timestamp: Date.now(),\n }\n\n // Legacy STEP events (kept during transition)\n yield {\n type: EventType.STEP_STARTED,\n stepName: stepId,\n stepId,\n model,\n timestamp: Date.now(),\n stepType: 'thinking',\n }\n }\n\n accumulatedThinking += part.text\n\n // Spec REASONING content event — reasoningMessageId is set in the\n // hasEmittedStepStarted block above (entered on the same `part.thought` path)\n if (!reasoningMessageId) continue\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningMessageId,\n delta: part.text,\n model,\n timestamp: Date.now(),\n }\n\n // Legacy STEP event\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId || generateId(this.name),\n stepId: stepId || generateId(this.name),\n model,\n timestamp: Date.now(),\n delta: part.text,\n content: accumulatedThinking,\n }\n } else if (part.text.trim()) {\n // Close reasoning before text starts\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n // Skip whitespace-only text parts (e.g. \"\\n\" during auto-continuation)\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += part.text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\n delta: part.text,\n content: accumulatedContent,\n }\n }\n }\n\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n // Gemini emits thoughtSignature as a Part-level sibling of\n // functionCall (per @google/genai Part type), not nested inside\n // functionCall itself.\n const partThoughtSignature = part.thoughtSignature || undefined\n\n let toolCallData = toolCallMap.get(toolCallId)\n if (!toolCallData) {\n toolCallData = {\n name: functionCall.name || '',\n args:\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs),\n index: nextToolIndex++,\n started: false,\n // Only set thoughtSignature when present — under EOPT, the\n // optional field cannot accept an explicit `undefined`.\n ...(partThoughtSignature !== undefined && {\n thoughtSignature: partThoughtSignature,\n }),\n }\n toolCallMap.set(toolCallId, toolCallData)\n } else {\n if (!toolCallData.thoughtSignature && partThoughtSignature) {\n toolCallData.thoughtSignature = partThoughtSignature\n }\n try {\n const existingArgs = JSON.parse(toolCallData.args)\n const newArgs =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n const mergedArgs = { ...existingArgs, ...newArgs }\n toolCallData.args = JSON.stringify(mergedArgs)\n } catch {\n toolCallData.args =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n }\n }\n\n // Emit TOOL_CALL_START if not already started\n if (!toolCallData.started) {\n toolCallData.started = true\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp: Date.now(),\n index: toolCallData.index,\n ...(toolCallData.thoughtSignature && {\n metadata: {\n thoughtSignature: toolCallData.thoughtSignature,\n } satisfies GeminiToolCallMetadata,\n }),\n }\n }\n\n // Emit TOOL_CALL_ARGS\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId,\n model,\n timestamp: Date.now(),\n delta: toolCallData.args,\n args: toolCallData.args,\n }\n }\n }\n } else if (chunk.data && chunk.data.trim()) {\n // Skip whitespace-only data (e.g. \"\\n\" during auto-continuation)\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += chunk.data\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\n delta: chunk.data,\n content: accumulatedContent,\n }\n }\n\n if (chunk.candidates?.[0]?.finishReason) {\n const finishReason = chunk.candidates[0].finishReason\n\n if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {\n if (chunk.candidates[0].content?.parts) {\n for (const part of chunk.candidates[0].content.parts) {\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n const argsString =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n\n toolCallMap.set(toolCallId, {\n name: functionCall.name || '',\n args: argsString,\n index: nextToolIndex++,\n started: true,\n })\n\n // Emit TOOL_CALL_START\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId,\n toolCallName: functionCall.name || '',\n toolName: functionCall.name || '',\n model,\n timestamp: Date.now(),\n index: nextToolIndex - 1,\n }\n\n // Emit TOOL_CALL_END with parsed input\n let parsedInput: unknown = {}\n try {\n const parsed =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n parsedInput =\n parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n parsedInput = {}\n }\n\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId,\n toolCallName: functionCall.name || '',\n toolName: functionCall.name || '',\n model,\n timestamp: Date.now(),\n input: parsedInput,\n }\n }\n }\n }\n }\n\n // Emit TOOL_CALL_END for all tracked tool calls\n for (const [toolCallId, toolCallData] of toolCallMap.entries()) {\n let parsedInput: unknown = {}\n try {\n const parsed = JSON.parse(toolCallData.args)\n parsedInput = parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n parsedInput = {}\n }\n\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp: Date.now(),\n input: parsedInput,\n }\n }\n\n // Reset so a new TEXT_MESSAGE_START is emitted if text follows tool calls\n if (toolCallMap.size > 0) {\n hasEmittedTextMessageStart = false\n }\n\n if (finishReason === FinishReason.MAX_TOKENS) {\n yield {\n type: EventType.RUN_ERROR,\n runId,\n model,\n timestamp: Date.now(),\n message:\n 'The response was cut off because the maximum token limit was reached.',\n code: 'max_tokens',\n error: {\n message:\n 'The response was cut off because the maximum token limit was reached.',\n code: 'max_tokens',\n },\n }\n }\n\n // Close reasoning events if still open\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',\n // RunFinishedEvent.usage is `usage?: {...}` (no `| undefined`) under\n // exactOptionalPropertyTypes; only include it when usageMetadata is\n // present rather than assigning an explicit `undefined`.\n ...(chunk.usageMetadata && {\n usage: {\n promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,\n completionTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,\n totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,\n },\n }),\n }\n }\n }\n }\n\n private convertContentPartToGemini(part: ContentPart): Part {\n switch (part.type) {\n case 'text':\n return { text: part.content }\n case 'image':\n case 'audio':\n case 'video':\n case 'document': {\n if (part.source.type === 'data') {\n return {\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n }\n } else {\n // For URL sources, use provided mimeType or fall back to reasonable defaults\n const defaultMimeType = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n }[part.type]\n\n return {\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? defaultMimeType,\n },\n }\n }\n }\n default: {\n const _exhaustiveCheck: never = part\n throw new Error(\n `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`,\n )\n }\n }\n }\n\n private formatMessages(\n messages: Array<ModelMessage>,\n ): GenerateContentParameters['contents'] {\n // Build a lookup from toolCallId → function name so functionResponse uses the\n // correct name instead of the raw call ID.\n const toolCallIdToName = new Map<string, string>()\n for (const msg of messages) {\n if (msg.role === 'assistant' && msg.toolCalls) {\n for (const tc of msg.toolCalls) {\n toolCallIdToName.set(tc.id, tc.function.name)\n }\n }\n }\n\n const formatted = messages.map((msg) => {\n const role: 'user' | 'model' = msg.role === 'assistant' ? 'model' : 'user'\n const parts: Array<Part> = []\n\n if (Array.isArray(msg.content)) {\n for (const contentPart of msg.content) {\n parts.push(this.convertContentPartToGemini(contentPart))\n }\n } else if (msg.content && msg.role !== 'tool') {\n parts.push({ text: msg.content })\n }\n\n if (msg.role === 'assistant' && msg.toolCalls?.length) {\n for (const toolCall of msg.toolCalls) {\n let parsedArgs: Record<string, unknown> = {}\n try {\n parsedArgs = toolCall.function.arguments\n ? (JSON.parse(toolCall.function.arguments) as Record<\n string,\n unknown\n >)\n : {}\n } catch {\n parsedArgs = {}\n }\n\n const thoughtSignature = (\n toolCall.metadata as GeminiToolCallMetadata | undefined\n )?.thoughtSignature\n // Gemini requires thoughtSignature at the Part level (sibling of\n // functionCall), not nested inside functionCall. Nesting it causes\n // the API to reject the next turn with\n // \"Function call is missing a thought_signature\".\n const part: Part = {\n functionCall: {\n id: toolCall.id,\n name: toolCall.function.name,\n args: parsedArgs,\n },\n }\n if (thoughtSignature) {\n part.thoughtSignature = thoughtSignature\n }\n parts.push(part)\n }\n }\n\n if (msg.role === 'tool' && msg.toolCallId) {\n const functionName =\n toolCallIdToName.get(msg.toolCallId) || msg.toolCallId\n const toolContent = msg.content\n if (Array.isArray(toolContent)) {\n const textChunks: Array<string> = []\n const mediaParts: Array<Part> = []\n for (const part of toolContent) {\n if (part.type === 'text') {\n textChunks.push(part.content)\n } else if (part.source.type === 'data') {\n mediaParts.push({\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n })\n } else {\n const defaultMimeType = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n }[part.type]\n mediaParts.push({\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? defaultMimeType,\n },\n })\n }\n }\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: { content: textChunks.join('\\n') },\n ...(mediaParts.length > 0 && { parts: mediaParts }),\n },\n })\n } else {\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: { content: toolContent || '' },\n },\n })\n }\n }\n\n return {\n role,\n parts: parts.length > 0 ? parts : [{ text: '' }],\n }\n })\n\n // Post-process: Gemini requires strictly alternating user/model roles.\n // Tool results are mapped to role:'user', which can create consecutive\n // user messages when followed by a new user message. Merge them.\n return this.mergeConsecutiveSameRoleMessages(formatted)\n }\n\n /**\n * Merge consecutive messages of the same role into a single message.\n * Gemini's API requires strictly alternating user/model roles.\n * Tool results are mapped to role:'user', which can collide with actual\n * user messages in multi-turn conversations.\n *\n * Also filters out empty model messages (e.g., from a previous failed request)\n * and deduplicates functionResponse parts with the same name (tool call ID).\n */\n private mergeConsecutiveSameRoleMessages(\n messages: Array<Content>,\n ): Array<Content> {\n const merged: Array<Content> = []\n\n for (const msg of messages) {\n const parts = msg.parts || []\n\n // Skip empty model messages (no parts or only empty text)\n if (msg.role === 'model') {\n const hasContent =\n parts.length > 0 &&\n !parts.every(\n (p) => 'text' in p && (p as { text: string }).text === '',\n )\n if (!hasContent) {\n continue\n }\n }\n\n const prev = merged[merged.length - 1]\n if (prev && prev.role === msg.role) {\n // Merge parts arrays\n prev.parts = [...(prev.parts || []), ...parts]\n } else {\n merged.push({ ...msg, parts: [...parts] })\n }\n }\n\n // Deduplicate functionResponse parts with the same name (tool call ID)\n for (const msg of merged) {\n if (!msg.parts) continue\n const seenFunctionResponseNames = new Set<string>()\n msg.parts = msg.parts.filter((part) => {\n if ('functionResponse' in part && part.functionResponse?.name) {\n if (seenFunctionResponseNames.has(part.functionResponse.name)) {\n return false\n }\n seenFunctionResponseNames.add(part.functionResponse.name)\n }\n return true\n })\n }\n\n return merged\n }\n\n private mapCommonOptionsToGemini(\n options: TextOptions<GeminiTextProviderOptions>,\n ) {\n // Separate `thinkingConfig` from the other model options so the loose\n // local `thinkingLevel?: keyof typeof ThinkingLevel` type doesn't leak\n // into the SDK config object via the `...modelOpts` spread — we re-add a\n // properly-typed `ThinkingConfig` below.\n const { thinkingConfig, ...modelOpts } = options.modelOptions ?? {}\n // Build the thinkingConfig payload only when the caller actually supplied\n // one. Our local `thinkingLevel` is typed as `keyof typeof ThinkingLevel`\n // (string union) so users can pass plain strings; the SDK target is the\n // `ThinkingLevel` enum, and every field is `field?: T` under EOPT — so we\n // re-emit fields via conditional spreads.\n const mappedThinkingConfig = thinkingConfig\n ? {\n ...(thinkingConfig.includeThoughts !== undefined && {\n includeThoughts: thinkingConfig.includeThoughts,\n }),\n ...(thinkingConfig.thinkingBudget !== undefined && {\n thinkingBudget: thinkingConfig.thinkingBudget,\n }),\n ...(thinkingConfig.thinkingLevel\n ? {\n thinkingLevel: thinkingConfig.thinkingLevel as ThinkingLevel,\n }\n : {}),\n }\n : undefined\n\n const normalizedPrompts = normalizeSystemPrompts(options.systemPrompts)\n const systemInstruction =\n normalizedPrompts.length > 0\n ? normalizedPrompts.map((p) => p.content).join('\\n')\n : undefined\n\n // Native combined mode (issue #605): when the engine threads\n // `outputSchema` through TextOptions, the adapter declared\n // `supportsCombinedToolsAndSchema` (Gemini 3.x only). The schema is\n // already JSON Schema (pre-converted at the activity boundary). Wire\n // it into `config.responseSchema` + `responseMimeType: 'application/json'`\n // alongside any `tools` — the model emits function calls during the\n // agent loop and the schema-constrained JSON on its natural final\n // turn, so the engine can harvest it without the separate\n // `structuredOutput` finalization round-trip.\n const combinedSchema = options.outputSchema as\n | Record<string, unknown>\n | undefined\n const combinedSchemaConfig = combinedSchema\n ? {\n responseMimeType: 'application/json' as const,\n responseSchema: combinedSchema,\n }\n : undefined\n\n // Vendor `GenerateContentConfig` fields are `field?: T` (no `| undefined`)\n // under EOPT, so spread each common option only when present rather than\n // emitting `field: undefined`s into the wire payload.\n const requestOptions: GenerateContentParameters = {\n model: options.model,\n contents: this.formatMessages(options.messages),\n config: {\n ...modelOpts,\n ...(options.temperature !== undefined && {\n temperature: options.temperature,\n }),\n ...(options.topP !== undefined && { topP: options.topP }),\n ...(options.maxTokens !== undefined && {\n maxOutputTokens: options.maxTokens,\n }),\n ...(mappedThinkingConfig !== undefined && {\n thinkingConfig: mappedThinkingConfig,\n }),\n ...(systemInstruction !== undefined && { systemInstruction }),\n tools: convertToolsToProviderFormat(options.tools),\n ...(combinedSchemaConfig ?? {}),\n },\n }\n\n return requestOptions\n }\n\n /**\n * Gemini 3.x natively combines `tools` + `responseSchema` in a single\n * streaming `generateContentStream` call (issue #605). Gemini 2.x is\n * documented as brittle for the combination and keeps the engine's\n * legacy finalization path.\n */\n supportsCombinedToolsAndSchema(): boolean {\n return GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS.has(this.model)\n }\n}\n\n/**\n * Creates a Gemini text adapter with explicit API key.\n * Type resolution happens here at the call site.\n */\nexport function createGeminiChat<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>,\n ResolveToolCapabilities<TModel>\n> {\n return new GeminiTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini text adapter with automatic API key detection.\n * Type resolution happens here at the call site.\n */\nexport function geminiText<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>,\n ResolveToolCapabilities<TModel>\n> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiChat(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"text.js","sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { convertToolsToProviderFormat } from '../tools/tool-converter'\nimport { buildGeminiUsage } from '../usage'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS } from '../model-meta'\nimport type {\n GEMINI_MODELS,\n GeminiChatModelProviderOptionsByName,\n GeminiChatModelToolCapabilitiesByName,\n GeminiModelInputModalitiesByName,\n} from '../model-meta'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type {\n Content,\n GenerateContentParameters,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n ThinkingLevel,\n} from '@google/genai'\nimport type {\n ContentPart,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { ExternalTextProviderOptions } from '../text/text-provider-options'\nimport type {\n GeminiMessageMetadataByModality,\n GeminiToolCallMetadata,\n} from '../message-types'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for Gemini text adapter\n */\nexport interface GeminiTextConfig extends GeminiClientConfig {}\n\n/**\n * Gemini-specific provider options for text/chat\n */\nexport type GeminiTextProviderOptions = ExternalTextProviderOptions\n\n// ===========================\n// Type Resolution Helpers\n// ===========================\n\n/**\n * Resolve provider options for a specific model.\n * If the model has explicit options in the map, use those; otherwise use base options.\n */\ntype ResolveProviderOptions<TModel extends string> =\n TModel extends keyof GeminiChatModelProviderOptionsByName\n ? GeminiChatModelProviderOptionsByName[TModel]\n : GeminiTextProviderOptions\n\n/**\n * Resolve input modalities for a specific model.\n * If the model has explicit modalities in the map, use those; otherwise use all modalities.\n */\ntype ResolveInputModalities<TModel extends string> =\n TModel extends keyof GeminiModelInputModalitiesByName\n ? GeminiModelInputModalitiesByName[TModel]\n : readonly ['text', 'image', 'audio', 'video', 'document']\n\n/**\n * Resolve tool capabilities for a specific model.\n * If the model has explicit tools in the map, use those; otherwise use empty tuple.\n */\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof GeminiChatModelToolCapabilitiesByName\n ? NonNullable<GeminiChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n// ===========================\n// Adapter Implementation\n// ===========================\n\n/**\n * Gemini Text (Chat) Adapter\n *\n * Tree-shakeable adapter for Gemini chat/text completion functionality.\n * Import only what you need for smaller bundle sizes.\n */\nexport class GeminiTextAdapter<\n TModel extends (typeof GEMINI_MODELS)[number],\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GeminiMessageMetadataByModality,\n TToolCapabilities,\n GeminiToolCallMetadata\n> {\n override readonly kind = 'text' as const\n readonly name = 'gemini' as const\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiTextConfig, model: TModel) {\n super({}, model)\n this.client = createGeminiClient(config)\n }\n\n async *chatStream(\n options: TextOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const mappedOptions = this.mapCommonOptionsToGemini(options)\n const { logger } = options\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,\n { provider: 'gemini', model: this.model },\n )\n const result =\n await this.client.models.generateContentStream(mappedOptions)\n\n yield* this.processStreamChunks(result, options, logger)\n } catch (error) {\n const rawEvent = toRunErrorRawEvent(error)\n logger.errors('gemini.chatStream fatal', {\n error,\n source: 'gemini.chatStream',\n })\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n // Forward the provider's structured error body when present (see\n // toRunErrorRawEvent); omitted otherwise.\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n },\n }\n }\n }\n\n /**\n * Generate structured output using Gemini's native JSON response format.\n * Uses responseMimeType: 'application/json' and responseSchema for structured output.\n * The outputSchema is already JSON Schema (converted in the ai layer).\n */\n async structuredOutput(\n options: StructuredOutputOptions<GeminiTextProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const { chatOptions, outputSchema } = options\n const { logger } = chatOptions\n\n const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${this.model} messages=${chatOptions.messages.length} tools=${chatOptions.tools?.length ?? 0} stream=false`,\n { provider: 'gemini', model: this.model },\n )\n // Add structured output configuration\n const result = await this.client.models.generateContent({\n ...mappedOptions,\n config: {\n ...mappedOptions.config,\n responseMimeType: 'application/json',\n responseSchema: outputSchema,\n },\n })\n\n // Extract text content from the response\n const rawText = this.extractTextFromResponse(result)\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n return {\n data: parsed,\n rawText,\n usage: result.usageMetadata\n ? buildGeminiUsage(result.usageMetadata)\n : undefined,\n }\n } catch (error) {\n logger.errors('gemini.structuredOutput fatal', {\n error,\n source: 'gemini.structuredOutput',\n })\n throw new Error(\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during structured output generation.',\n )\n }\n }\n\n /**\n * Extract text content from a non-streaming response\n */\n private extractTextFromResponse(response: GenerateContentResponse): string {\n let textContent = ''\n\n if (response.candidates?.[0]?.content?.parts) {\n for (const part of response.candidates[0].content.parts) {\n if (part.text) {\n textContent += part.text\n }\n }\n }\n\n return textContent\n }\n\n private async *processStreamChunks(\n result: AsyncGenerator<GenerateContentResponse, unknown, unknown>,\n options: TextOptions<GeminiTextProviderOptions>,\n logger: InternalLogger,\n ): AsyncIterable<StreamChunk> {\n const model = options.model\n let accumulatedContent = ''\n let accumulatedThinking = ''\n const toolCallMap = new Map<\n string,\n {\n name: string\n args: string\n index: number\n started: boolean\n thoughtSignature?: string\n }\n >()\n let nextToolIndex = 0\n\n // AG-UI lifecycle tracking\n const runId = options.runId ?? generateId(this.name)\n const threadId = options.threadId ?? generateId(this.name)\n const messageId = generateId(this.name)\n let stepId: string | null = null\n let reasoningMessageId: string | null = null\n let hasClosedReasoning = false\n let hasEmittedRunStarted = false\n let hasEmittedTextMessageStart = false\n let hasEmittedStepStarted = false\n\n for await (const chunk of result) {\n logger.provider(`provider=gemini`, { chunk })\n // Emit RUN_STARTED on first chunk\n if (!hasEmittedRunStarted) {\n hasEmittedRunStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\n }\n }\n\n if (chunk.candidates?.[0]?.content?.parts) {\n const parts = chunk.candidates[0].content.parts\n\n for (const part of parts) {\n if (part.text) {\n if (part.thought) {\n // Emit STEP_STARTED and REASONING events on first thinking content\n if (!hasEmittedStepStarted) {\n hasEmittedStepStarted = true\n stepId = generateId(this.name)\n reasoningMessageId = generateId(this.name)\n\n // Spec REASONING events\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model,\n timestamp: Date.now(),\n }\n\n // Legacy STEP events (kept during transition)\n yield {\n type: EventType.STEP_STARTED,\n stepName: stepId,\n stepId,\n model,\n timestamp: Date.now(),\n stepType: 'thinking',\n }\n }\n\n accumulatedThinking += part.text\n\n // Spec REASONING content event — reasoningMessageId is set in the\n // hasEmittedStepStarted block above (entered on the same `part.thought` path)\n if (!reasoningMessageId) continue\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningMessageId,\n delta: part.text,\n model,\n timestamp: Date.now(),\n }\n\n // Legacy STEP event\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId || generateId(this.name),\n stepId: stepId || generateId(this.name),\n model,\n timestamp: Date.now(),\n delta: part.text,\n content: accumulatedThinking,\n }\n } else if (part.text.trim()) {\n // Close reasoning before text starts\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n // Skip whitespace-only text parts (e.g. \"\\n\" during auto-continuation)\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += part.text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\n delta: part.text,\n content: accumulatedContent,\n }\n }\n }\n\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n // Gemini emits thoughtSignature as a Part-level sibling of\n // functionCall (per @google/genai Part type), not nested inside\n // functionCall itself.\n const partThoughtSignature = part.thoughtSignature || undefined\n\n let toolCallData = toolCallMap.get(toolCallId)\n if (!toolCallData) {\n toolCallData = {\n name: functionCall.name || '',\n args:\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs),\n index: nextToolIndex++,\n started: false,\n // Only set thoughtSignature when present — under EOPT, the\n // optional field cannot accept an explicit `undefined`.\n ...(partThoughtSignature !== undefined && {\n thoughtSignature: partThoughtSignature,\n }),\n }\n toolCallMap.set(toolCallId, toolCallData)\n } else {\n if (!toolCallData.thoughtSignature && partThoughtSignature) {\n toolCallData.thoughtSignature = partThoughtSignature\n }\n try {\n const existingArgs = JSON.parse(toolCallData.args)\n const newArgs =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n const mergedArgs = { ...existingArgs, ...newArgs }\n toolCallData.args = JSON.stringify(mergedArgs)\n } catch {\n toolCallData.args =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n }\n }\n\n // Emit TOOL_CALL_START if not already started\n if (!toolCallData.started) {\n toolCallData.started = true\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp: Date.now(),\n index: toolCallData.index,\n ...(toolCallData.thoughtSignature && {\n metadata: {\n thoughtSignature: toolCallData.thoughtSignature,\n } satisfies GeminiToolCallMetadata,\n }),\n }\n }\n\n // Emit TOOL_CALL_ARGS\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId,\n model,\n timestamp: Date.now(),\n delta: toolCallData.args,\n args: toolCallData.args,\n }\n }\n }\n } else if (chunk.data && chunk.data.trim()) {\n // Skip whitespace-only data (e.g. \"\\n\" during auto-continuation)\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += chunk.data\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\n delta: chunk.data,\n content: accumulatedContent,\n }\n }\n\n if (chunk.candidates?.[0]?.finishReason) {\n const finishReason = chunk.candidates[0].finishReason\n\n if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {\n if (chunk.candidates[0].content?.parts) {\n for (const part of chunk.candidates[0].content.parts) {\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n const argsString =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n\n toolCallMap.set(toolCallId, {\n name: functionCall.name || '',\n args: argsString,\n index: nextToolIndex++,\n started: true,\n })\n\n // Emit TOOL_CALL_START\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId,\n toolCallName: functionCall.name || '',\n toolName: functionCall.name || '',\n model,\n timestamp: Date.now(),\n index: nextToolIndex - 1,\n }\n\n // Emit TOOL_CALL_END with parsed input\n let parsedInput: unknown = {}\n try {\n const parsed =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n parsedInput =\n parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n parsedInput = {}\n }\n\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId,\n toolCallName: functionCall.name || '',\n toolName: functionCall.name || '',\n model,\n timestamp: Date.now(),\n input: parsedInput,\n }\n }\n }\n }\n }\n\n // Emit TOOL_CALL_END for all tracked tool calls\n for (const [toolCallId, toolCallData] of toolCallMap.entries()) {\n let parsedInput: unknown = {}\n try {\n const parsed = JSON.parse(toolCallData.args)\n parsedInput = parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n parsedInput = {}\n }\n\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp: Date.now(),\n input: parsedInput,\n }\n }\n\n // Reset so a new TEXT_MESSAGE_START is emitted if text follows tool calls\n if (toolCallMap.size > 0) {\n hasEmittedTextMessageStart = false\n }\n\n if (finishReason === FinishReason.MAX_TOKENS) {\n yield {\n type: EventType.RUN_ERROR,\n runId,\n model,\n timestamp: Date.now(),\n message:\n 'The response was cut off because the maximum token limit was reached.',\n code: 'max_tokens',\n error: {\n message:\n 'The response was cut off because the maximum token limit was reached.',\n code: 'max_tokens',\n },\n }\n }\n\n // Close reasoning events if still open\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',\n // RunFinishedEvent.usage is `usage?: {...}` (no `| undefined`) under\n // exactOptionalPropertyTypes; only include it when usageMetadata is\n // present rather than assigning an explicit `undefined`.\n ...(chunk.usageMetadata && {\n usage: buildGeminiUsage(chunk.usageMetadata),\n }),\n }\n }\n }\n }\n\n private convertContentPartToGemini(part: ContentPart): Part {\n switch (part.type) {\n case 'text':\n return { text: part.content }\n case 'image':\n case 'audio':\n case 'video':\n case 'document': {\n if (part.source.type === 'data') {\n return {\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n }\n } else {\n // For URL sources, use provided mimeType or fall back to reasonable defaults\n const defaultMimeType = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n }[part.type]\n\n return {\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? defaultMimeType,\n },\n }\n }\n }\n default: {\n const _exhaustiveCheck: never = part\n throw new Error(\n `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`,\n )\n }\n }\n }\n\n private formatMessages(\n messages: Array<ModelMessage>,\n ): GenerateContentParameters['contents'] {\n // Build a lookup from toolCallId → function name so functionResponse uses the\n // correct name instead of the raw call ID.\n const toolCallIdToName = new Map<string, string>()\n for (const msg of messages) {\n if (msg.role === 'assistant' && msg.toolCalls) {\n for (const tc of msg.toolCalls) {\n toolCallIdToName.set(tc.id, tc.function.name)\n }\n }\n }\n\n const formatted = messages.map((msg) => {\n const role: 'user' | 'model' = msg.role === 'assistant' ? 'model' : 'user'\n const parts: Array<Part> = []\n\n if (Array.isArray(msg.content)) {\n for (const contentPart of msg.content) {\n parts.push(this.convertContentPartToGemini(contentPart))\n }\n } else if (msg.content && msg.role !== 'tool') {\n parts.push({ text: msg.content })\n }\n\n if (msg.role === 'assistant' && msg.toolCalls?.length) {\n for (const toolCall of msg.toolCalls) {\n let parsedArgs: Record<string, unknown> = {}\n try {\n parsedArgs = toolCall.function.arguments\n ? (JSON.parse(toolCall.function.arguments) as Record<\n string,\n unknown\n >)\n : {}\n } catch {\n parsedArgs = {}\n }\n\n const thoughtSignature = (\n toolCall.metadata as GeminiToolCallMetadata | undefined\n )?.thoughtSignature\n // Gemini requires thoughtSignature at the Part level (sibling of\n // functionCall), not nested inside functionCall. Nesting it causes\n // the API to reject the next turn with\n // \"Function call is missing a thought_signature\".\n const part: Part = {\n functionCall: {\n id: toolCall.id,\n name: toolCall.function.name,\n args: parsedArgs,\n },\n }\n if (thoughtSignature) {\n part.thoughtSignature = thoughtSignature\n }\n parts.push(part)\n }\n }\n\n if (msg.role === 'tool' && msg.toolCallId) {\n const functionName =\n toolCallIdToName.get(msg.toolCallId) || msg.toolCallId\n const toolContent = msg.content\n if (Array.isArray(toolContent)) {\n const textChunks: Array<string> = []\n const mediaParts: Array<Part> = []\n for (const part of toolContent) {\n if (part.type === 'text') {\n textChunks.push(part.content)\n } else if (part.source.type === 'data') {\n mediaParts.push({\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n })\n } else {\n const defaultMimeType = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n }[part.type]\n mediaParts.push({\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? defaultMimeType,\n },\n })\n }\n }\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: { content: textChunks.join('\\n') },\n ...(mediaParts.length > 0 && { parts: mediaParts }),\n },\n })\n } else {\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: { content: toolContent || '' },\n },\n })\n }\n }\n\n return {\n role,\n parts: parts.length > 0 ? parts : [{ text: '' }],\n }\n })\n\n // Post-process: Gemini requires strictly alternating user/model roles.\n // Tool results are mapped to role:'user', which can create consecutive\n // user messages when followed by a new user message. Merge them.\n return this.mergeConsecutiveSameRoleMessages(formatted)\n }\n\n /**\n * Merge consecutive messages of the same role into a single message.\n * Gemini's API requires strictly alternating user/model roles.\n * Tool results are mapped to role:'user', which can collide with actual\n * user messages in multi-turn conversations.\n *\n * Also filters out empty model messages (e.g., from a previous failed request)\n * and deduplicates functionResponse parts with the same name (tool call ID).\n */\n private mergeConsecutiveSameRoleMessages(\n messages: Array<Content>,\n ): Array<Content> {\n const merged: Array<Content> = []\n\n for (const msg of messages) {\n const parts = msg.parts || []\n\n // Skip empty model messages (no parts or only empty text)\n if (msg.role === 'model') {\n const hasContent =\n parts.length > 0 &&\n !parts.every(\n (p) => 'text' in p && (p as { text: string }).text === '',\n )\n if (!hasContent) {\n continue\n }\n }\n\n const prev = merged[merged.length - 1]\n if (prev && prev.role === msg.role) {\n // Merge parts arrays\n prev.parts = [...(prev.parts || []), ...parts]\n } else {\n merged.push({ ...msg, parts: [...parts] })\n }\n }\n\n // Deduplicate functionResponse parts with the same name (tool call ID)\n for (const msg of merged) {\n if (!msg.parts) continue\n const seenFunctionResponseNames = new Set<string>()\n msg.parts = msg.parts.filter((part) => {\n if ('functionResponse' in part && part.functionResponse?.name) {\n if (seenFunctionResponseNames.has(part.functionResponse.name)) {\n return false\n }\n seenFunctionResponseNames.add(part.functionResponse.name)\n }\n return true\n })\n }\n\n return merged\n }\n\n private mapCommonOptionsToGemini(\n options: TextOptions<GeminiTextProviderOptions>,\n ) {\n // Separate `thinkingConfig` from the other model options so the loose\n // local `thinkingLevel?: keyof typeof ThinkingLevel` type doesn't leak\n // into the SDK config object via the `...modelOpts` spread — we re-add a\n // properly-typed `ThinkingConfig` below.\n const { thinkingConfig, ...modelOpts } = options.modelOptions ?? {}\n // Build the thinkingConfig payload only when the caller actually supplied\n // one. Our local `thinkingLevel` is typed as `keyof typeof ThinkingLevel`\n // (string union) so users can pass plain strings; the SDK target is the\n // `ThinkingLevel` enum, and every field is `field?: T` under EOPT — so we\n // re-emit fields via conditional spreads.\n const mappedThinkingConfig = thinkingConfig\n ? {\n ...(thinkingConfig.includeThoughts !== undefined && {\n includeThoughts: thinkingConfig.includeThoughts,\n }),\n ...(thinkingConfig.thinkingBudget !== undefined && {\n thinkingBudget: thinkingConfig.thinkingBudget,\n }),\n ...(thinkingConfig.thinkingLevel\n ? {\n thinkingLevel: thinkingConfig.thinkingLevel as ThinkingLevel,\n }\n : {}),\n }\n : undefined\n\n const normalizedPrompts = normalizeSystemPrompts(options.systemPrompts)\n const systemInstruction =\n normalizedPrompts.length > 0\n ? normalizedPrompts.map((p) => p.content).join('\\n')\n : undefined\n\n // Native combined mode (issue #605): when the engine threads\n // `outputSchema` through TextOptions, the adapter declared\n // `supportsCombinedToolsAndSchema` (Gemini 3.x only). The schema is\n // already JSON Schema (pre-converted at the activity boundary). Wire\n // it into `config.responseSchema` + `responseMimeType: 'application/json'`\n // alongside any `tools` — the model emits function calls during the\n // agent loop and the schema-constrained JSON on its natural final\n // turn, so the engine can harvest it without the separate\n // `structuredOutput` finalization round-trip.\n const combinedSchema = options.outputSchema as\n | Record<string, unknown>\n | undefined\n const combinedSchemaConfig = combinedSchema\n ? {\n responseMimeType: 'application/json' as const,\n responseSchema: combinedSchema,\n }\n : undefined\n\n // Vendor `GenerateContentConfig` fields are `field?: T` (no `| undefined`)\n // under EOPT, so spread each common option only when present rather than\n // emitting `field: undefined`s into the wire payload.\n const requestOptions: GenerateContentParameters = {\n model: options.model,\n contents: this.formatMessages(options.messages),\n config: {\n ...modelOpts,\n ...(options.temperature !== undefined && {\n temperature: options.temperature,\n }),\n ...(options.topP !== undefined && { topP: options.topP }),\n ...(options.maxTokens !== undefined && {\n maxOutputTokens: options.maxTokens,\n }),\n ...(mappedThinkingConfig !== undefined && {\n thinkingConfig: mappedThinkingConfig,\n }),\n ...(systemInstruction !== undefined && { systemInstruction }),\n tools: convertToolsToProviderFormat(options.tools),\n ...(combinedSchemaConfig ?? {}),\n },\n }\n\n return requestOptions\n }\n\n /**\n * Gemini 3.x natively combines `tools` + `responseSchema` in a single\n * streaming `generateContentStream` call (issue #605). Gemini 2.x is\n * documented as brittle for the combination and keeps the engine's\n * legacy finalization path.\n */\n supportsCombinedToolsAndSchema(): boolean {\n return GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS.has(this.model)\n }\n}\n\n/**\n * Creates a Gemini text adapter with explicit API key.\n * Type resolution happens here at the call site.\n */\nexport function createGeminiChat<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>,\n ResolveToolCapabilities<TModel>\n> {\n return new GeminiTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini text adapter with automatic API key detection.\n * Type resolution happens here at the call site.\n */\nexport function geminiText<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>,\n ResolveToolCapabilities<TModel>\n> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiChat(model, apiKey, config)\n}\n"],"names":[],"mappings":";;;;;;;;AAgGO,MAAM,0BAOH,gBAOR;AAAA,EACkB,OAAO;AAAA,EAChB,OAAO;AAAA,EAEC;AAAA,EAEjB,YAAY,QAA0B,OAAe;AACnD,UAAM,CAAA,GAAI,KAAK;AACf,SAAK,SAAS,mBAAmB,MAAM;AAAA,EACzC;AAAA,EAEA,OAAO,WACL,SAC4B;AAC5B,UAAM,gBAAgB,KAAK,yBAAyB,OAAO;AAC3D,UAAM,EAAE,WAAW;AAEnB,QAAI;AACF,aAAO;AAAA,QACL,uCAAuC,KAAK,KAAK,aAAa,QAAQ,SAAS,MAAM,UAAU,QAAQ,OAAO,UAAU,CAAC;AAAA,QACzH,EAAE,UAAU,UAAU,OAAO,KAAK,MAAA;AAAA,MAAM;AAE1C,YAAM,SACJ,MAAM,KAAK,OAAO,OAAO,sBAAsB,aAAa;AAE9D,aAAO,KAAK,oBAAoB,QAAQ,SAAS,MAAM;AAAA,IACzD,SAAS,OAAO;AACd,YAAM,WAAW,mBAAmB,KAAK;AACzC,aAAO,OAAO,2BAA2B;AAAA,QACvC;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,QACJ,MAAM,UAAU;AAAA,QAChB,OAAO,QAAQ;AAAA,QACf,WAAW,KAAK,IAAA;AAAA,QAChB,SACE,iBAAiB,QACb,MAAM,UACN;AAAA;AAAA;AAAA,QAGN,GAAI,aAAa,UAAa,EAAE,SAAA;AAAA,QAChC,OAAO;AAAA,UACL,SACE,iBAAiB,QACb,MAAM,UACN;AAAA,QAAA;AAAA,MACR;AAAA,IAEJ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,iBACJ,SAC0C;AAC1C,UAAM,EAAE,aAAa,aAAA,IAAiB;AACtC,UAAM,EAAE,WAAW;AAEnB,UAAM,gBAAgB,KAAK,yBAAyB,WAAW;AAE/D,QAAI;AACF,aAAO;AAAA,QACL,uCAAuC,KAAK,KAAK,aAAa,YAAY,SAAS,MAAM,UAAU,YAAY,OAAO,UAAU,CAAC;AAAA,QACjI,EAAE,UAAU,UAAU,OAAO,KAAK,MAAA;AAAA,MAAM;AAG1C,YAAM,SAAS,MAAM,KAAK,OAAO,OAAO,gBAAgB;AAAA,QACtD,GAAG;AAAA,QACH,QAAQ;AAAA,UACN,GAAG,cAAc;AAAA,UACjB,kBAAkB;AAAA,UAClB,gBAAgB;AAAA,QAAA;AAAA,MAClB,CACD;AAGD,YAAM,UAAU,KAAK,wBAAwB,MAAM;AAGnD,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,OAAO;AAAA,MAC7B,QAAQ;AACN,cAAM,IAAI;AAAA,UACR,uDAAuD,QAAQ,MAAM,GAAG,GAAG,CAAC,GAAG,QAAQ,SAAS,MAAM,QAAQ,EAAE;AAAA,QAAA;AAAA,MAEpH;AAEA,aAAO;AAAA,QACL,MAAM;AAAA,QACN;AAAA,QACA,OAAO,OAAO,gBACV,iBAAiB,OAAO,aAAa,IACrC;AAAA,MAAA;AAAA,IAER,SAAS,OAAO;AACd,aAAO,OAAO,iCAAiC;AAAA,QAC7C;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM,IAAI;AAAA,QACR,iBAAiB,QACb,MAAM,UACN;AAAA,MAAA;AAAA,IAER;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,wBAAwB,UAA2C;AACzE,QAAI,cAAc;AAElB,QAAI,SAAS,aAAa,CAAC,GAAG,SAAS,OAAO;AAC5C,iBAAW,QAAQ,SAAS,WAAW,CAAC,EAAE,QAAQ,OAAO;AACvD,YAAI,KAAK,MAAM;AACb,yBAAe,KAAK;AAAA,QACtB;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,OAAe,oBACb,QACA,SACA,QAC4B;AAC5B,UAAM,QAAQ,QAAQ;AACtB,QAAI,qBAAqB;AACzB,QAAI,sBAAsB;AAC1B,UAAM,kCAAkB,IAAA;AAUxB,QAAI,gBAAgB;AAGpB,UAAM,QAAQ,QAAQ,SAAS,WAAW,KAAK,IAAI;AACnD,UAAM,WAAW,QAAQ,YAAY,WAAW,KAAK,IAAI;AACzD,UAAM,YAAY,WAAW,KAAK,IAAI;AACtC,QAAI,SAAwB;AAC5B,QAAI,qBAAoC;AACxC,QAAI,qBAAqB;AACzB,QAAI,uBAAuB;AAC3B,QAAI,6BAA6B;AACjC,QAAI,wBAAwB;AAE5B,qBAAiB,SAAS,QAAQ;AAChC,aAAO,SAAS,mBAAmB,EAAE,MAAA,CAAO;AAE5C,UAAI,CAAC,sBAAsB;AACzB,+BAAuB;AACvB,cAAM;AAAA,UACJ,MAAM,UAAU;AAAA,UAChB;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,KAAK,IAAA;AAAA,UAChB,aAAa,QAAQ;AAAA,QAAA;AAAA,MAEzB;AAEA,UAAI,MAAM,aAAa,CAAC,GAAG,SAAS,OAAO;AACzC,cAAM,QAAQ,MAAM,WAAW,CAAC,EAAE,QAAQ;AAE1C,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,MAAM;AACb,gBAAI,KAAK,SAAS;AAEhB,kBAAI,CAAC,uBAAuB;AAC1B,wCAAwB;AACxB,yBAAS,WAAW,KAAK,IAAI;AAC7B,qCAAqB,WAAW,KAAK,IAAI;AAGzC,sBAAM;AAAA,kBACJ,MAAM,UAAU;AAAA,kBAChB,WAAW;AAAA,kBACX;AAAA,kBACA,WAAW,KAAK,IAAA;AAAA,gBAAI;AAEtB,sBAAM;AAAA,kBACJ,MAAM,UAAU;AAAA,kBAChB,WAAW;AAAA,kBACX,MAAM;AAAA,kBACN;AAAA,kBACA,WAAW,KAAK,IAAA;AAAA,gBAAI;AAItB,sBAAM;AAAA,kBACJ,MAAM,UAAU;AAAA,kBAChB,UAAU;AAAA,kBACV;AAAA,kBACA;AAAA,kBACA,WAAW,KAAK,IAAA;AAAA,kBAChB,UAAU;AAAA,gBAAA;AAAA,cAEd;AAEA,qCAAuB,KAAK;AAI5B,kBAAI,CAAC,mBAAoB;AACzB,oBAAM;AAAA,gBACJ,MAAM,UAAU;AAAA,gBAChB,WAAW;AAAA,gBACX,OAAO,KAAK;AAAA,gBACZ;AAAA,gBACA,WAAW,KAAK,IAAA;AAAA,cAAI;AAItB,oBAAM;AAAA,gBACJ,MAAM,UAAU;AAAA,gBAChB,UAAU,UAAU,WAAW,KAAK,IAAI;AAAA,gBACxC,QAAQ,UAAU,WAAW,KAAK,IAAI;AAAA,gBACtC;AAAA,gBACA,WAAW,KAAK,IAAA;AAAA,gBAChB,OAAO,KAAK;AAAA,gBACZ,SAAS;AAAA,cAAA;AAAA,YAEb,WAAW,KAAK,KAAK,KAAA,GAAQ;AAE3B,kBAAI,sBAAsB,CAAC,oBAAoB;AAC7C,qCAAqB;AACrB,sBAAM;AAAA,kBACJ,MAAM,UAAU;AAAA,kBAChB,WAAW;AAAA,kBACX;AAAA,kBACA,WAAW,KAAK,IAAA;AAAA,gBAAI;AAEtB,sBAAM;AAAA,kBACJ,MAAM,UAAU;AAAA,kBAChB,WAAW;AAAA,kBACX;AAAA,kBACA,WAAW,KAAK,IAAA;AAAA,gBAAI;AAAA,cAExB;AAIA,kBAAI,CAAC,4BAA4B;AAC/B,6CAA6B;AAC7B,sBAAM;AAAA,kBACJ,MAAM,UAAU;AAAA,kBAChB;AAAA,kBACA;AAAA,kBACA,WAAW,KAAK,IAAA;AAAA,kBAChB,MAAM;AAAA,gBAAA;AAAA,cAEV;AAEA,oCAAsB,KAAK;AAC3B,oBAAM;AAAA,gBACJ,MAAM,UAAU;AAAA,gBAChB;AAAA,gBACA;AAAA,gBACA,WAAW,KAAK,IAAA;AAAA,gBAChB,OAAO,KAAK;AAAA,gBACZ,SAAS;AAAA,cAAA;AAAA,YAEb;AAAA,UACF;AAEA,gBAAM,eAAe,KAAK;AAC1B,cAAI,cAAc;AAChB,kBAAM,aACJ,aAAa,MACb,GAAG,aAAa,IAAI,IAAI,KAAK,IAAA,CAAK,IAAI,aAAa;AACrD,kBAAM,eAAe,aAAa,QAAQ,CAAA;AAK1C,kBAAM,uBAAuB,KAAK,oBAAoB;AAEtD,gBAAI,eAAe,YAAY,IAAI,UAAU;AAC7C,gBAAI,CAAC,cAAc;AACjB,6BAAe;AAAA,gBACb,MAAM,aAAa,QAAQ;AAAA,gBAC3B,MACE,OAAO,iBAAiB,WACpB,eACA,KAAK,UAAU,YAAY;AAAA,gBACjC,OAAO;AAAA,gBACP,SAAS;AAAA;AAAA;AAAA,gBAGT,GAAI,yBAAyB,UAAa;AAAA,kBACxC,kBAAkB;AAAA,gBAAA;AAAA,cACpB;AAEF,0BAAY,IAAI,YAAY,YAAY;AAAA,YAC1C,OAAO;AACL,kBAAI,CAAC,aAAa,oBAAoB,sBAAsB;AAC1D,6BAAa,mBAAmB;AAAA,cAClC;AACA,kBAAI;AACF,sBAAM,eAAe,KAAK,MAAM,aAAa,IAAI;AACjD,sBAAM,UACJ,OAAO,iBAAiB,WACpB,KAAK,MAAM,YAAY,IACvB;AACN,sBAAM,aAAa,EAAE,GAAG,cAAc,GAAG,QAAA;AACzC,6BAAa,OAAO,KAAK,UAAU,UAAU;AAAA,cAC/C,QAAQ;AACN,6BAAa,OACX,OAAO,iBAAiB,WACpB,eACA,KAAK,UAAU,YAAY;AAAA,cACnC;AAAA,YACF;AAGA,gBAAI,CAAC,aAAa,SAAS;AACzB,2BAAa,UAAU;AACvB,oBAAM;AAAA,gBACJ,MAAM,UAAU;AAAA,gBAChB;AAAA,gBACA,cAAc,aAAa;AAAA,gBAC3B,UAAU,aAAa;AAAA,gBACvB;AAAA,gBACA,WAAW,KAAK,IAAA;AAAA,gBAChB,OAAO,aAAa;AAAA,gBACpB,GAAI,aAAa,oBAAoB;AAAA,kBACnC,UAAU;AAAA,oBACR,kBAAkB,aAAa;AAAA,kBAAA;AAAA,gBACjC;AAAA,cACF;AAAA,YAEJ;AAGA,kBAAM;AAAA,cACJ,MAAM,UAAU;AAAA,cAChB;AAAA,cACA;AAAA,cACA,WAAW,KAAK,IAAA;AAAA,cAChB,OAAO,aAAa;AAAA,cACpB,MAAM,aAAa;AAAA,YAAA;AAAA,UAEvB;AAAA,QACF;AAAA,MACF,WAAW,MAAM,QAAQ,MAAM,KAAK,QAAQ;AAG1C,YAAI,CAAC,4BAA4B;AAC/B,uCAA6B;AAC7B,gBAAM;AAAA,YACJ,MAAM,UAAU;AAAA,YAChB;AAAA,YACA;AAAA,YACA,WAAW,KAAK,IAAA;AAAA,YAChB,MAAM;AAAA,UAAA;AAAA,QAEV;AAEA,8BAAsB,MAAM;AAC5B,cAAM;AAAA,UACJ,MAAM,UAAU;AAAA,UAChB;AAAA,UACA;AAAA,UACA,WAAW,KAAK,IAAA;AAAA,UAChB,OAAO,MAAM;AAAA,UACb,SAAS;AAAA,QAAA;AAAA,MAEb;AAEA,UAAI,MAAM,aAAa,CAAC,GAAG,cAAc;AACvC,cAAM,eAAe,MAAM,WAAW,CAAC,EAAE;AAEzC,YAAI,iBAAiB,aAAa,sBAAsB;AACtD,cAAI,MAAM,WAAW,CAAC,EAAE,SAAS,OAAO;AACtC,uBAAW,QAAQ,MAAM,WAAW,CAAC,EAAE,QAAQ,OAAO;AACpD,oBAAM,eAAe,KAAK;AAC1B,kBAAI,cAAc;AAChB,sBAAM,aACJ,aAAa,MACb,GAAG,aAAa,IAAI,IAAI,KAAK,IAAA,CAAK,IAAI,aAAa;AACrD,sBAAM,eAAe,aAAa,QAAQ,CAAA;AAE1C,sBAAM,aACJ,OAAO,iBAAiB,WACpB,eACA,KAAK,UAAU,YAAY;AAEjC,4BAAY,IAAI,YAAY;AAAA,kBAC1B,MAAM,aAAa,QAAQ;AAAA,kBAC3B,MAAM;AAAA,kBACN,OAAO;AAAA,kBACP,SAAS;AAAA,gBAAA,CACV;AAGD,sBAAM;AAAA,kBACJ,MAAM,UAAU;AAAA,kBAChB;AAAA,kBACA,cAAc,aAAa,QAAQ;AAAA,kBACnC,UAAU,aAAa,QAAQ;AAAA,kBAC/B;AAAA,kBACA,WAAW,KAAK,IAAA;AAAA,kBAChB,OAAO,gBAAgB;AAAA,gBAAA;AAIzB,oBAAI,cAAuB,CAAA;AAC3B,oBAAI;AACF,wBAAM,SACJ,OAAO,iBAAiB,WACpB,KAAK,MAAM,YAAY,IACvB;AACN,gCACE,UAAU,OAAO,WAAW,WAAW,SAAS,CAAA;AAAA,gBACpD,QAAQ;AACN,gCAAc,CAAA;AAAA,gBAChB;AAEA,sBAAM;AAAA,kBACJ,MAAM,UAAU;AAAA,kBAChB;AAAA,kBACA,cAAc,aAAa,QAAQ;AAAA,kBACnC,UAAU,aAAa,QAAQ;AAAA,kBAC/B;AAAA,kBACA,WAAW,KAAK,IAAA;AAAA,kBAChB,OAAO;AAAA,gBAAA;AAAA,cAEX;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAGA,mBAAW,CAAC,YAAY,YAAY,KAAK,YAAY,WAAW;AAC9D,cAAI,cAAuB,CAAA;AAC3B,cAAI;AACF,kBAAM,SAAS,KAAK,MAAM,aAAa,IAAI;AAC3C,0BAAc,UAAU,OAAO,WAAW,WAAW,SAAS,CAAA;AAAA,UAChE,QAAQ;AACN,0BAAc,CAAA;AAAA,UAChB;AAEA,gBAAM;AAAA,YACJ,MAAM,UAAU;AAAA,YAChB;AAAA,YACA,cAAc,aAAa;AAAA,YAC3B,UAAU,aAAa;AAAA,YACvB;AAAA,YACA,WAAW,KAAK,IAAA;AAAA,YAChB,OAAO;AAAA,UAAA;AAAA,QAEX;AAGA,YAAI,YAAY,OAAO,GAAG;AACxB,uCAA6B;AAAA,QAC/B;AAEA,YAAI,iBAAiB,aAAa,YAAY;AAC5C,gBAAM;AAAA,YACJ,MAAM,UAAU;AAAA,YAChB;AAAA,YACA;AAAA,YACA,WAAW,KAAK,IAAA;AAAA,YAChB,SACE;AAAA,YACF,MAAM;AAAA,YACN,OAAO;AAAA,cACL,SACE;AAAA,cACF,MAAM;AAAA,YAAA;AAAA,UACR;AAAA,QAEJ;AAGA,YAAI,sBAAsB,CAAC,oBAAoB;AAC7C,+BAAqB;AACrB,gBAAM;AAAA,YACJ,MAAM,UAAU;AAAA,YAChB,WAAW;AAAA,YACX;AAAA,YACA,WAAW,KAAK,IAAA;AAAA,UAAI;AAEtB,gBAAM;AAAA,YACJ,MAAM,UAAU;AAAA,YAChB,WAAW;AAAA,YACX;AAAA,YACA,WAAW,KAAK,IAAA;AAAA,UAAI;AAAA,QAExB;AAGA,YAAI,4BAA4B;AAC9B,gBAAM;AAAA,YACJ,MAAM,UAAU;AAAA,YAChB;AAAA,YACA;AAAA,YACA,WAAW,KAAK,IAAA;AAAA,UAAI;AAAA,QAExB;AAEA,cAAM;AAAA,UACJ,MAAM,UAAU;AAAA,UAChB;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,KAAK,IAAA;AAAA,UAChB,cAAc,YAAY,OAAO,IAAI,eAAe;AAAA;AAAA;AAAA;AAAA,UAIpD,GAAI,MAAM,iBAAiB;AAAA,YACzB,OAAO,iBAAiB,MAAM,aAAa;AAAA,UAAA;AAAA,QAC7C;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,2BAA2B,MAAyB;AAC1D,YAAQ,KAAK,MAAA;AAAA,MACX,KAAK;AACH,eAAO,EAAE,MAAM,KAAK,QAAA;AAAA,MACtB,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK,YAAY;AACf,YAAI,KAAK,OAAO,SAAS,QAAQ;AAC/B,iBAAO;AAAA,YACL,YAAY;AAAA,cACV,MAAM,KAAK,OAAO;AAAA,cAClB,UAAU,KAAK,OAAO;AAAA,YAAA;AAAA,UACxB;AAAA,QAEJ,OAAO;AAEL,gBAAM,kBAAkB;AAAA,YACtB,OAAO;AAAA,YACP,OAAO;AAAA,YACP,OAAO;AAAA,YACP,UAAU;AAAA,UAAA,EACV,KAAK,IAAI;AAEX,iBAAO;AAAA,YACL,UAAU;AAAA,cACR,SAAS,KAAK,OAAO;AAAA,cACrB,UAAU,KAAK,OAAO,YAAY;AAAA,YAAA;AAAA,UACpC;AAAA,QAEJ;AAAA,MACF;AAAA,MACA,SAAS;AACP,cAAM,mBAA0B;AAChC,cAAM,IAAI;AAAA,UACR,kCAAmC,iBAAiC,IAAI;AAAA,QAAA;AAAA,MAE5E;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEQ,eACN,UACuC;AAGvC,UAAM,uCAAuB,IAAA;AAC7B,eAAW,OAAO,UAAU;AAC1B,UAAI,IAAI,SAAS,eAAe,IAAI,WAAW;AAC7C,mBAAW,MAAM,IAAI,WAAW;AAC9B,2BAAiB,IAAI,GAAG,IAAI,GAAG,SAAS,IAAI;AAAA,QAC9C;AAAA,MACF;AAAA,IACF;AAEA,UAAM,YAAY,SAAS,IAAI,CAAC,QAAQ;AACtC,YAAM,OAAyB,IAAI,SAAS,cAAc,UAAU;AACpE,YAAM,QAAqB,CAAA;AAE3B,UAAI,MAAM,QAAQ,IAAI,OAAO,GAAG;AAC9B,mBAAW,eAAe,IAAI,SAAS;AACrC,gBAAM,KAAK,KAAK,2BAA2B,WAAW,CAAC;AAAA,QACzD;AAAA,MACF,WAAW,IAAI,WAAW,IAAI,SAAS,QAAQ;AAC7C,cAAM,KAAK,EAAE,MAAM,IAAI,SAAS;AAAA,MAClC;AAEA,UAAI,IAAI,SAAS,eAAe,IAAI,WAAW,QAAQ;AACrD,mBAAW,YAAY,IAAI,WAAW;AACpC,cAAI,aAAsC,CAAA;AAC1C,cAAI;AACF,yBAAa,SAAS,SAAS,YAC1B,KAAK,MAAM,SAAS,SAAS,SAAS,IAIvC,CAAA;AAAA,UACN,QAAQ;AACN,yBAAa,CAAA;AAAA,UACf;AAEA,gBAAM,mBACJ,SAAS,UACR;AAKH,gBAAM,OAAa;AAAA,YACjB,cAAc;AAAA,cACZ,IAAI,SAAS;AAAA,cACb,MAAM,SAAS,SAAS;AAAA,cACxB,MAAM;AAAA,YAAA;AAAA,UACR;AAEF,cAAI,kBAAkB;AACpB,iBAAK,mBAAmB;AAAA,UAC1B;AACA,gBAAM,KAAK,IAAI;AAAA,QACjB;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,UAAU,IAAI,YAAY;AACzC,cAAM,eACJ,iBAAiB,IAAI,IAAI,UAAU,KAAK,IAAI;AAC9C,cAAM,cAAc,IAAI;AACxB,YAAI,MAAM,QAAQ,WAAW,GAAG;AAC9B,gBAAM,aAA4B,CAAA;AAClC,gBAAM,aAA0B,CAAA;AAChC,qBAAW,QAAQ,aAAa;AAC9B,gBAAI,KAAK,SAAS,QAAQ;AACxB,yBAAW,KAAK,KAAK,OAAO;AAAA,YAC9B,WAAW,KAAK,OAAO,SAAS,QAAQ;AACtC,yBAAW,KAAK;AAAA,gBACd,YAAY;AAAA,kBACV,MAAM,KAAK,OAAO;AAAA,kBAClB,UAAU,KAAK,OAAO;AAAA,gBAAA;AAAA,cACxB,CACD;AAAA,YACH,OAAO;AACL,oBAAM,kBAAkB;AAAA,gBACtB,OAAO;AAAA,gBACP,OAAO;AAAA,gBACP,OAAO;AAAA,gBACP,UAAU;AAAA,cAAA,EACV,KAAK,IAAI;AACX,yBAAW,KAAK;AAAA,gBACd,UAAU;AAAA,kBACR,SAAS,KAAK,OAAO;AAAA,kBACrB,UAAU,KAAK,OAAO,YAAY;AAAA,gBAAA;AAAA,cACpC,CACD;AAAA,YACH;AAAA,UACF;AACA,gBAAM,KAAK;AAAA,YACT,kBAAkB;AAAA,cAChB,IAAI,IAAI;AAAA,cACR,MAAM;AAAA,cACN,UAAU,EAAE,SAAS,WAAW,KAAK,IAAI,EAAA;AAAA,cACzC,GAAI,WAAW,SAAS,KAAK,EAAE,OAAO,WAAA;AAAA,YAAW;AAAA,UACnD,CACD;AAAA,QACH,OAAO;AACL,gBAAM,KAAK;AAAA,YACT,kBAAkB;AAAA,cAChB,IAAI,IAAI;AAAA,cACR,MAAM;AAAA,cACN,UAAU,EAAE,SAAS,eAAe,GAAA;AAAA,YAAG;AAAA,UACzC,CACD;AAAA,QACH;AAAA,MACF;AAEA,aAAO;AAAA,QACL;AAAA,QACA,OAAO,MAAM,SAAS,IAAI,QAAQ,CAAC,EAAE,MAAM,GAAA,CAAI;AAAA,MAAA;AAAA,IAEnD,CAAC;AAKD,WAAO,KAAK,iCAAiC,SAAS;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,iCACN,UACgB;AAChB,UAAM,SAAyB,CAAA;AAE/B,eAAW,OAAO,UAAU;AAC1B,YAAM,QAAQ,IAAI,SAAS,CAAA;AAG3B,UAAI,IAAI,SAAS,SAAS;AACxB,cAAM,aACJ,MAAM,SAAS,KACf,CAAC,MAAM;AAAA,UACL,CAAC,MAAM,UAAU,KAAM,EAAuB,SAAS;AAAA,QAAA;AAE3D,YAAI,CAAC,YAAY;AACf;AAAA,QACF;AAAA,MACF;AAEA,YAAM,OAAO,OAAO,OAAO,SAAS,CAAC;AACrC,UAAI,QAAQ,KAAK,SAAS,IAAI,MAAM;AAElC,aAAK,QAAQ,CAAC,GAAI,KAAK,SAAS,CAAA,GAAK,GAAG,KAAK;AAAA,MAC/C,OAAO;AACL,eAAO,KAAK,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,KAAK,GAAG;AAAA,MAC3C;AAAA,IACF;AAGA,eAAW,OAAO,QAAQ;AACxB,UAAI,CAAC,IAAI,MAAO;AAChB,YAAM,gDAAgC,IAAA;AACtC,UAAI,QAAQ,IAAI,MAAM,OAAO,CAAC,SAAS;AACrC,YAAI,sBAAsB,QAAQ,KAAK,kBAAkB,MAAM;AAC7D,cAAI,0BAA0B,IAAI,KAAK,iBAAiB,IAAI,GAAG;AAC7D,mBAAO;AAAA,UACT;AACA,oCAA0B,IAAI,KAAK,iBAAiB,IAAI;AAAA,QAC1D;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,yBACN,SACA;AAKA,UAAM,EAAE,gBAAgB,GAAG,cAAc,QAAQ,gBAAgB,CAAA;AAMjE,UAAM,uBAAuB,iBACzB;AAAA,MACE,GAAI,eAAe,oBAAoB,UAAa;AAAA,QAClD,iBAAiB,eAAe;AAAA,MAAA;AAAA,MAElC,GAAI,eAAe,mBAAmB,UAAa;AAAA,QACjD,gBAAgB,eAAe;AAAA,MAAA;AAAA,MAEjC,GAAI,eAAe,gBACf;AAAA,QACE,eAAe,eAAe;AAAA,MAAA,IAEhC,CAAA;AAAA,IAAC,IAEP;AAEJ,UAAM,oBAAoB,uBAAuB,QAAQ,aAAa;AACtE,UAAM,oBACJ,kBAAkB,SAAS,IACvB,kBAAkB,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,IAAI,IACjD;AAWN,UAAM,iBAAiB,QAAQ;AAG/B,UAAM,uBAAuB,iBACzB;AAAA,MACE,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,IAAA,IAElB;AAKJ,UAAM,iBAA4C;AAAA,MAChD,OAAO,QAAQ;AAAA,MACf,UAAU,KAAK,eAAe,QAAQ,QAAQ;AAAA,MAC9C,QAAQ;AAAA,QACN,GAAG;AAAA,QACH,GAAI,QAAQ,gBAAgB,UAAa;AAAA,UACvC,aAAa,QAAQ;AAAA,QAAA;AAAA,QAEvB,GAAI,QAAQ,SAAS,UAAa,EAAE,MAAM,QAAQ,KAAA;AAAA,QAClD,GAAI,QAAQ,cAAc,UAAa;AAAA,UACrC,iBAAiB,QAAQ;AAAA,QAAA;AAAA,QAE3B,GAAI,yBAAyB,UAAa;AAAA,UACxC,gBAAgB;AAAA,QAAA;AAAA,QAElB,GAAI,sBAAsB,UAAa,EAAE,kBAAA;AAAA,QACzC,OAAO,6BAA6B,QAAQ,KAAK;AAAA,QACjD,GAAI,wBAAwB,CAAA;AAAA,MAAC;AAAA,IAC/B;AAGF,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iCAA0C;AACxC,WAAO,wCAAwC,IAAI,KAAK,KAAK;AAAA,EAC/D;AACF;AAMO,SAAS,iBACd,OACA,QACA,QAMA;AACA,SAAO,IAAI,kBAAkB,EAAE,QAAQ,GAAG,OAAA,GAAU,KAAK;AAC3D;AAMO,SAAS,WACd,OACA,QAMA;AACA,QAAM,SAAS,uBAAA;AACf,SAAO,iBAAiB,OAAO,QAAQ,MAAM;AAC/C;"}
|
package/dist/esm/adapters/tts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseTTSAdapter } from "@tanstack/ai/adapters";
|
|
2
2
|
import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils/client.js";
|
|
3
3
|
import { GEMINI_TTS_VOICES } from "../model-meta.js";
|
|
4
|
+
import { buildGeminiUsage } from "../usage.js";
|
|
4
5
|
class GeminiTTSAdapter extends BaseTTSAdapter {
|
|
5
6
|
name = "gemini";
|
|
6
7
|
client;
|
|
@@ -77,6 +78,7 @@ class GeminiTTSAdapter extends BaseTTSAdapter {
|
|
|
77
78
|
}
|
|
78
79
|
const audioBase64 = audioPart.inlineData.data;
|
|
79
80
|
const mimeType = audioPart.inlineData.mimeType;
|
|
81
|
+
const usageField = response.usageMetadata ? { usage: buildGeminiUsage(response.usageMetadata) } : {};
|
|
80
82
|
const pcm = parsePcmMimeType(mimeType);
|
|
81
83
|
if (pcm) {
|
|
82
84
|
const wavBase64 = wrapPcmBase64AsWav(
|
|
@@ -90,7 +92,8 @@ class GeminiTTSAdapter extends BaseTTSAdapter {
|
|
|
90
92
|
model,
|
|
91
93
|
audio: wavBase64,
|
|
92
94
|
format: "wav",
|
|
93
|
-
contentType: "audio/wav"
|
|
95
|
+
contentType: "audio/wav",
|
|
96
|
+
...usageField
|
|
94
97
|
};
|
|
95
98
|
}
|
|
96
99
|
const format = (mimeType.split(";")[0] ?? "").split("/")[1] || "wav";
|
|
@@ -99,7 +102,8 @@ class GeminiTTSAdapter extends BaseTTSAdapter {
|
|
|
99
102
|
model,
|
|
100
103
|
audio: audioBase64,
|
|
101
104
|
format,
|
|
102
|
-
contentType: mimeType
|
|
105
|
+
contentType: mimeType,
|
|
106
|
+
...usageField
|
|
103
107
|
};
|
|
104
108
|
} catch (error) {
|
|
105
109
|
logger.errors("gemini.generateSpeech fatal", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tts.js","sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { GEMINI_TTS_VOICES } from '../model-meta'\nimport type { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GoogleGenAI, SpeechConfig } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for a single speaker in a multi-speaker dialogue.\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n */\nexport interface GeminiSpeakerVoiceConfig {\n /** A name used in the prompt to refer to this speaker */\n speaker: string\n /** Voice configuration for this speaker */\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: GeminiTTSVoice\n }\n }\n}\n\n/**\n * Provider-specific options for Gemini TTS\n *\n * @experimental Gemini TTS is an experimental feature.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n * @see https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-tts-preview\n */\nexport interface GeminiTTSProviderOptions {\n /**\n * Voice configuration for single-speaker TTS.\n * Choose from 30 available voices with different characteristics.\n *\n * Use `multiSpeakerVoiceConfig` instead for dialogues.\n */\n voiceConfig?: {\n prebuiltVoiceConfig?: {\n /**\n * The voice name to use for speech synthesis.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation#voices\n */\n voiceName?: GeminiTTSVoice\n }\n }\n\n /**\n * Multi-speaker voice configuration (up to 2 speakers).\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n *\n * Each speaker's lines in the prompt are prefixed with the name defined\n * here, e.g.:\n *\n * ```text\n * Joe: Hey, how's it going?\n * Jane: Not bad, you?\n * ```\n */\n multiSpeakerVoiceConfig?: {\n speakerVoiceConfigs: Array<GeminiSpeakerVoiceConfig>\n }\n\n /**\n * System instruction for controlling speech style.\n * Use natural language to describe the desired speaking style,\n * pace, tone, accent, or other characteristics.\n *\n * With Gemini 3.1 Flash TTS, you can also use inline audio tags like\n * `[whispering]`, `[laughs]`, `[excited]` directly in the input text\n * to control delivery.\n *\n * @example \"Speak slowly and calmly, as if telling a bedtime story\"\n * @example \"Use an upbeat, enthusiastic tone with moderate pace\"\n * @example \"Speak with a British accent\"\n */\n systemInstruction?: string\n\n /**\n * Language code hint for the speech synthesis.\n * Gemini 3.1 Flash TTS supports 70+ languages with auto-detection;\n * the 2.5 TTS models support 24 languages.\n *\n * @example \"en-US\" for American English\n * @example \"es-ES\" for Spanish (Spain)\n * @example \"ja-JP\" for Japanese\n */\n languageCode?: string\n}\n\n/**\n * Configuration for Gemini TTS adapter\n *\n * @experimental Gemini TTS is an experimental feature.\n */\nexport interface GeminiTTSConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini TTS */\nexport type GeminiTTSModel = (typeof GEMINI_TTS_MODELS)[number]\n\n/**\n * Gemini Text-to-Speech Adapter\n *\n * Tree-shakeable adapter for Gemini TTS functionality.\n *\n * **IMPORTANT**: Gemini TTS uses the Live API (WebSocket-based) which requires\n * different handling than traditional REST APIs. This adapter provides a\n * simplified interface but may have limitations.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Models:\n * - gemini-2.5-flash-preview-tts\n */\nexport class GeminiTTSAdapter<\n TModel extends GeminiTTSModel,\n> extends BaseTTSAdapter<TModel, GeminiTTSProviderOptions> {\n readonly name = 'gemini' as const\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiTTSConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n /**\n * Generate speech from text using Gemini's TTS model.\n *\n * @experimental This implementation is experimental and may change.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n */\n async generateSpeech(\n options: TTSOptions<GeminiTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { model, text, modelOptions, voice, logger } = options\n\n logger.request(`activity=generateSpeech provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n const speechConfig: SpeechConfig = {}\n\n if (modelOptions?.multiSpeakerVoiceConfig) {\n // Validate multi-speaker config: 1 or 2 speakers allowed.\n const speakerConfigs =\n modelOptions.multiSpeakerVoiceConfig.speakerVoiceConfigs\n if (\n !Array.isArray(speakerConfigs) ||\n speakerConfigs.length < 1 ||\n speakerConfigs.length > 2\n ) {\n throw new Error(\n `Gemini TTS multiSpeakerVoiceConfig.speakerVoiceConfigs must contain 1 or 2 speakers; received ${Array.isArray(speakerConfigs) ? speakerConfigs.length : 'non-array'}.`,\n )\n }\n speechConfig.multiSpeakerVoiceConfig =\n modelOptions.multiSpeakerVoiceConfig\n } else {\n // Honor the standard TTSOptions.voice (used by every other TTS adapter)\n // as a fallback for the prebuilt voice name. If an explicit\n // modelOptions.voiceConfig is supplied its values win — but we still\n // fall back to `voice` / 'Kore' if the supplied voiceConfig is missing\n // prebuiltVoiceConfig.voiceName.\n if (\n voice !== undefined &&\n !(GEMINI_TTS_VOICES as ReadonlyArray<string>).includes(voice)\n ) {\n throw new Error(\n `Invalid Gemini TTS voice \"${voice}\". Valid voices are: ${GEMINI_TTS_VOICES.join(', ')}.`,\n )\n }\n const defaultVoiceName = (voice as GeminiTTSVoice | undefined) ?? 'Kore'\n const supplied = modelOptions?.voiceConfig\n const resolvedVoiceName =\n supplied?.prebuiltVoiceConfig?.voiceName ?? defaultVoiceName\n speechConfig.voiceConfig = {\n prebuiltVoiceConfig: { voiceName: resolvedVoiceName },\n }\n }\n\n if (modelOptions?.languageCode) {\n speechConfig.languageCode = modelOptions.languageCode\n }\n\n try {\n const response = await this.client.models.generateContent({\n model,\n contents: [\n {\n role: 'user',\n parts: [{ text }],\n },\n ],\n config: {\n responseModalities: ['AUDIO'],\n speechConfig,\n // systemInstruction belongs inside `config` per the @google/genai\n // contract — matches sibling Gemini adapters (summarize, text).\n ...(modelOptions?.systemInstruction && {\n systemInstruction: modelOptions.systemInstruction,\n }),\n },\n })\n\n // Extract audio data from response\n const candidate = response.candidates?.[0]\n const parts = candidate?.content?.parts\n\n if (!parts || parts.length === 0) {\n throw new Error('No audio output received from Gemini TTS')\n }\n\n // Look for inline data (audio)\n const audioPart = parts.find((part: any) =>\n part.inlineData?.mimeType?.startsWith('audio/'),\n )\n\n if (!audioPart || !audioPart.inlineData || !audioPart.inlineData.data) {\n throw new Error('No audio data in Gemini TTS response')\n }\n\n const audioBase64 = audioPart.inlineData.data\n // mime is guaranteed by the `startsWith('audio/')` find predicate above.\n const mimeType = audioPart.inlineData.mimeType as string\n\n // Gemini TTS models return raw 16-bit LE PCM with a mime type like\n // `audio/L16;codec=pcm;rate=24000`. That isn't playable in an <audio>\n // element and the bare string isn't a usable file extension, so we\n // prepend a RIFF/WAV header here and normalize the result to audio/wav.\n const pcm = parsePcmMimeType(mimeType)\n if (pcm) {\n const wavBase64 = wrapPcmBase64AsWav(\n audioBase64,\n pcm.sampleRate,\n pcm.channels,\n pcm.bitsPerSample,\n )\n return {\n id: generateId(this.name),\n model,\n audio: wavBase64,\n format: 'wav',\n contentType: 'audio/wav',\n }\n }\n\n // Strip any mime parameters (e.g. `audio/ogg;codec=opus`) before pulling\n // the subtype out as the file format. `String.split` always returns at\n // least one element, so [0] is defined.\n const format = (mimeType.split(';')[0] ?? '').split('/')[1] || 'wav'\n\n return {\n id: generateId(this.name),\n model,\n audio: audioBase64,\n format,\n contentType: mimeType,\n }\n } catch (error) {\n logger.errors('gemini.generateSpeech fatal', {\n error,\n source: 'gemini.generateSpeech',\n })\n throw error\n }\n }\n}\n\nfunction parsePcmMimeType(\n mimeType: string,\n): { sampleRate: number; channels: number; bitsPerSample: number } | undefined {\n const normalized = mimeType.toLowerCase()\n const subtype = (normalized.split(';')[0] ?? '').split('/')[1] ?? ''\n // Exclude containerized wav (e.g. `audio/wav;codec=pcm`) — those already\n // carry a RIFF header and must not be re-wrapped.\n if (subtype.includes('wav')) return undefined\n\n // Accept the variants Gemini and other providers actually emit:\n // - audio/L16;codec=pcm;rate=24000 (IANA PCM with bit depth in the type)\n // - audio/L24 and friends\n // - audio/pcm and audio/x-pcm\n // - anything else that explicitly tags codec=pcm and isn't wav-containered\n const bitDepthMatch = /^audio\\/l(\\d+)/.exec(normalized)\n const isPcm =\n bitDepthMatch !== null ||\n normalized.startsWith('audio/pcm') ||\n normalized.startsWith('audio/x-pcm') ||\n normalized.includes('codec=pcm')\n if (!isPcm) return undefined\n\n const rateMatch = /rate=(\\d+)/.exec(normalized)\n const channelsMatch = /channels=(\\d+)/.exec(normalized)\n // Default to 16-bit when the mime type doesn't specify — matches Gemini's\n // audio/L16;codec=pcm;rate=24000 response.\n const bitsPerSample = bitDepthMatch ? Number(bitDepthMatch[1]) : 16\n return {\n sampleRate: rateMatch ? Number(rateMatch[1]) : 24000,\n channels: channelsMatch ? Number(channelsMatch[1]) : 1,\n bitsPerSample,\n }\n}\n\nfunction wrapPcmBase64AsWav(\n pcmBase64: string,\n sampleRate: number,\n channels = 1,\n bitsPerSample = 16,\n): string {\n // The WAV writer below emits a 16-bit PCM fmt chunk. If the source claims a\n // different bit depth we'd be lying about the payload, so bail out loudly\n // rather than producing a corrupt file.\n if (bitsPerSample !== 16) {\n throw new Error(\n `Unsupported PCM bit depth ${bitsPerSample}: only 16-bit PCM can be wrapped as WAV.`,\n )\n }\n\n const pcmBytes =\n typeof Buffer !== 'undefined'\n ? new Uint8Array(Buffer.from(pcmBase64, 'base64'))\n : decodeBase64(pcmBase64)\n\n const byteRate = (sampleRate * channels * bitsPerSample) / 8\n const blockAlign = (channels * bitsPerSample) / 8\n const dataSize = pcmBytes.byteLength\n const buffer = new ArrayBuffer(44 + dataSize)\n const view = new DataView(buffer)\n\n writeAscii(view, 0, 'RIFF')\n view.setUint32(4, 36 + dataSize, true)\n writeAscii(view, 8, 'WAVE')\n writeAscii(view, 12, 'fmt ')\n view.setUint32(16, 16, true)\n view.setUint16(20, 1, true)\n view.setUint16(22, channels, true)\n view.setUint32(24, sampleRate, true)\n view.setUint32(28, byteRate, true)\n view.setUint16(32, blockAlign, true)\n view.setUint16(34, bitsPerSample, true)\n writeAscii(view, 36, 'data')\n view.setUint32(40, dataSize, true)\n new Uint8Array(buffer, 44).set(pcmBytes)\n\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(buffer).toString('base64')\n }\n let binary = ''\n const bytes = new Uint8Array(buffer)\n for (const byte of bytes) {\n binary += String.fromCharCode(byte)\n }\n return btoa(binary)\n}\n\nfunction decodeBase64(b64: string): Uint8Array {\n const binary = atob(b64)\n const out = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i)\n return out\n}\n\nfunction writeAscii(view: DataView, offset: number, text: string): void {\n for (let i = 0; i < text.length; i += 1) {\n view.setUint8(offset + i, text.charCodeAt(i))\n }\n}\n\n/**\n * Creates a Gemini TTS adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini TTS adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiSpeech('gemini-2.5-flash-preview-tts', \"your-api-key\");\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello, world!'\n * });\n * ```\n */\nexport function createGeminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n // Put apiKey LAST so caller-supplied config can't silently override the\n // explicit argument.\n return new GeminiTTSAdapter({ ...config, apiKey }, model)\n}\n\n/**\n * Creates a Gemini speech adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini speech adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiSpeech('gemini-2.5-flash-preview-tts');\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Welcome to TanStack AI!'\n * });\n * ```\n */\nexport function geminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiSpeech(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tts.js","sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { GEMINI_TTS_VOICES } from '../model-meta'\nimport { buildGeminiUsage } from '../usage'\nimport type { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GoogleGenAI, SpeechConfig } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for a single speaker in a multi-speaker dialogue.\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n */\nexport interface GeminiSpeakerVoiceConfig {\n /** A name used in the prompt to refer to this speaker */\n speaker: string\n /** Voice configuration for this speaker */\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: GeminiTTSVoice\n }\n }\n}\n\n/**\n * Provider-specific options for Gemini TTS\n *\n * @experimental Gemini TTS is an experimental feature.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n * @see https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-tts-preview\n */\nexport interface GeminiTTSProviderOptions {\n /**\n * Voice configuration for single-speaker TTS.\n * Choose from 30 available voices with different characteristics.\n *\n * Use `multiSpeakerVoiceConfig` instead for dialogues.\n */\n voiceConfig?: {\n prebuiltVoiceConfig?: {\n /**\n * The voice name to use for speech synthesis.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation#voices\n */\n voiceName?: GeminiTTSVoice\n }\n }\n\n /**\n * Multi-speaker voice configuration (up to 2 speakers).\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n *\n * Each speaker's lines in the prompt are prefixed with the name defined\n * here, e.g.:\n *\n * ```text\n * Joe: Hey, how's it going?\n * Jane: Not bad, you?\n * ```\n */\n multiSpeakerVoiceConfig?: {\n speakerVoiceConfigs: Array<GeminiSpeakerVoiceConfig>\n }\n\n /**\n * System instruction for controlling speech style.\n * Use natural language to describe the desired speaking style,\n * pace, tone, accent, or other characteristics.\n *\n * With Gemini 3.1 Flash TTS, you can also use inline audio tags like\n * `[whispering]`, `[laughs]`, `[excited]` directly in the input text\n * to control delivery.\n *\n * @example \"Speak slowly and calmly, as if telling a bedtime story\"\n * @example \"Use an upbeat, enthusiastic tone with moderate pace\"\n * @example \"Speak with a British accent\"\n */\n systemInstruction?: string\n\n /**\n * Language code hint for the speech synthesis.\n * Gemini 3.1 Flash TTS supports 70+ languages with auto-detection;\n * the 2.5 TTS models support 24 languages.\n *\n * @example \"en-US\" for American English\n * @example \"es-ES\" for Spanish (Spain)\n * @example \"ja-JP\" for Japanese\n */\n languageCode?: string\n}\n\n/**\n * Configuration for Gemini TTS adapter\n *\n * @experimental Gemini TTS is an experimental feature.\n */\nexport interface GeminiTTSConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini TTS */\nexport type GeminiTTSModel = (typeof GEMINI_TTS_MODELS)[number]\n\n/**\n * Gemini Text-to-Speech Adapter\n *\n * Tree-shakeable adapter for Gemini TTS functionality.\n *\n * **IMPORTANT**: Gemini TTS uses the Live API (WebSocket-based) which requires\n * different handling than traditional REST APIs. This adapter provides a\n * simplified interface but may have limitations.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Models:\n * - gemini-2.5-flash-preview-tts\n */\nexport class GeminiTTSAdapter<\n TModel extends GeminiTTSModel,\n> extends BaseTTSAdapter<TModel, GeminiTTSProviderOptions> {\n readonly name = 'gemini' as const\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiTTSConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n /**\n * Generate speech from text using Gemini's TTS model.\n *\n * @experimental This implementation is experimental and may change.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n */\n async generateSpeech(\n options: TTSOptions<GeminiTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { model, text, modelOptions, voice, logger } = options\n\n logger.request(`activity=generateSpeech provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n const speechConfig: SpeechConfig = {}\n\n if (modelOptions?.multiSpeakerVoiceConfig) {\n // Validate multi-speaker config: 1 or 2 speakers allowed.\n const speakerConfigs =\n modelOptions.multiSpeakerVoiceConfig.speakerVoiceConfigs\n if (\n !Array.isArray(speakerConfigs) ||\n speakerConfigs.length < 1 ||\n speakerConfigs.length > 2\n ) {\n throw new Error(\n `Gemini TTS multiSpeakerVoiceConfig.speakerVoiceConfigs must contain 1 or 2 speakers; received ${Array.isArray(speakerConfigs) ? speakerConfigs.length : 'non-array'}.`,\n )\n }\n speechConfig.multiSpeakerVoiceConfig =\n modelOptions.multiSpeakerVoiceConfig\n } else {\n // Honor the standard TTSOptions.voice (used by every other TTS adapter)\n // as a fallback for the prebuilt voice name. If an explicit\n // modelOptions.voiceConfig is supplied its values win — but we still\n // fall back to `voice` / 'Kore' if the supplied voiceConfig is missing\n // prebuiltVoiceConfig.voiceName.\n if (\n voice !== undefined &&\n !(GEMINI_TTS_VOICES as ReadonlyArray<string>).includes(voice)\n ) {\n throw new Error(\n `Invalid Gemini TTS voice \"${voice}\". Valid voices are: ${GEMINI_TTS_VOICES.join(', ')}.`,\n )\n }\n const defaultVoiceName = (voice as GeminiTTSVoice | undefined) ?? 'Kore'\n const supplied = modelOptions?.voiceConfig\n const resolvedVoiceName =\n supplied?.prebuiltVoiceConfig?.voiceName ?? defaultVoiceName\n speechConfig.voiceConfig = {\n prebuiltVoiceConfig: { voiceName: resolvedVoiceName },\n }\n }\n\n if (modelOptions?.languageCode) {\n speechConfig.languageCode = modelOptions.languageCode\n }\n\n try {\n const response = await this.client.models.generateContent({\n model,\n contents: [\n {\n role: 'user',\n parts: [{ text }],\n },\n ],\n config: {\n responseModalities: ['AUDIO'],\n speechConfig,\n // systemInstruction belongs inside `config` per the @google/genai\n // contract — matches sibling Gemini adapters (summarize, text).\n ...(modelOptions?.systemInstruction && {\n systemInstruction: modelOptions.systemInstruction,\n }),\n },\n })\n\n // Extract audio data from response\n const candidate = response.candidates?.[0]\n const parts = candidate?.content?.parts\n\n if (!parts || parts.length === 0) {\n throw new Error('No audio output received from Gemini TTS')\n }\n\n // Look for inline data (audio)\n const audioPart = parts.find((part: any) =>\n part.inlineData?.mimeType?.startsWith('audio/'),\n )\n\n if (!audioPart || !audioPart.inlineData || !audioPart.inlineData.data) {\n throw new Error('No audio data in Gemini TTS response')\n }\n\n const audioBase64 = audioPart.inlineData.data\n // mime is guaranteed by the `startsWith('audio/')` find predicate above.\n const mimeType = audioPart.inlineData.mimeType as string\n\n // Surface token usage (with per-modality breakdown) when Gemini reports\n // it. Spread conditionally for exactOptionalPropertyTypes — shared by both\n // the PCM→WAV and pass-through return paths below.\n const usageField = response.usageMetadata\n ? { usage: buildGeminiUsage(response.usageMetadata) }\n : {}\n\n // Gemini TTS models return raw 16-bit LE PCM with a mime type like\n // `audio/L16;codec=pcm;rate=24000`. That isn't playable in an <audio>\n // element and the bare string isn't a usable file extension, so we\n // prepend a RIFF/WAV header here and normalize the result to audio/wav.\n const pcm = parsePcmMimeType(mimeType)\n if (pcm) {\n const wavBase64 = wrapPcmBase64AsWav(\n audioBase64,\n pcm.sampleRate,\n pcm.channels,\n pcm.bitsPerSample,\n )\n return {\n id: generateId(this.name),\n model,\n audio: wavBase64,\n format: 'wav',\n contentType: 'audio/wav',\n ...usageField,\n }\n }\n\n // Strip any mime parameters (e.g. `audio/ogg;codec=opus`) before pulling\n // the subtype out as the file format. `String.split` always returns at\n // least one element, so [0] is defined.\n const format = (mimeType.split(';')[0] ?? '').split('/')[1] || 'wav'\n\n return {\n id: generateId(this.name),\n model,\n audio: audioBase64,\n format,\n contentType: mimeType,\n ...usageField,\n }\n } catch (error) {\n logger.errors('gemini.generateSpeech fatal', {\n error,\n source: 'gemini.generateSpeech',\n })\n throw error\n }\n }\n}\n\nfunction parsePcmMimeType(\n mimeType: string,\n): { sampleRate: number; channels: number; bitsPerSample: number } | undefined {\n const normalized = mimeType.toLowerCase()\n const subtype = (normalized.split(';')[0] ?? '').split('/')[1] ?? ''\n // Exclude containerized wav (e.g. `audio/wav;codec=pcm`) — those already\n // carry a RIFF header and must not be re-wrapped.\n if (subtype.includes('wav')) return undefined\n\n // Accept the variants Gemini and other providers actually emit:\n // - audio/L16;codec=pcm;rate=24000 (IANA PCM with bit depth in the type)\n // - audio/L24 and friends\n // - audio/pcm and audio/x-pcm\n // - anything else that explicitly tags codec=pcm and isn't wav-containered\n const bitDepthMatch = /^audio\\/l(\\d+)/.exec(normalized)\n const isPcm =\n bitDepthMatch !== null ||\n normalized.startsWith('audio/pcm') ||\n normalized.startsWith('audio/x-pcm') ||\n normalized.includes('codec=pcm')\n if (!isPcm) return undefined\n\n const rateMatch = /rate=(\\d+)/.exec(normalized)\n const channelsMatch = /channels=(\\d+)/.exec(normalized)\n // Default to 16-bit when the mime type doesn't specify — matches Gemini's\n // audio/L16;codec=pcm;rate=24000 response.\n const bitsPerSample = bitDepthMatch ? Number(bitDepthMatch[1]) : 16\n return {\n sampleRate: rateMatch ? Number(rateMatch[1]) : 24000,\n channels: channelsMatch ? Number(channelsMatch[1]) : 1,\n bitsPerSample,\n }\n}\n\nfunction wrapPcmBase64AsWav(\n pcmBase64: string,\n sampleRate: number,\n channels = 1,\n bitsPerSample = 16,\n): string {\n // The WAV writer below emits a 16-bit PCM fmt chunk. If the source claims a\n // different bit depth we'd be lying about the payload, so bail out loudly\n // rather than producing a corrupt file.\n if (bitsPerSample !== 16) {\n throw new Error(\n `Unsupported PCM bit depth ${bitsPerSample}: only 16-bit PCM can be wrapped as WAV.`,\n )\n }\n\n const pcmBytes =\n typeof Buffer !== 'undefined'\n ? new Uint8Array(Buffer.from(pcmBase64, 'base64'))\n : decodeBase64(pcmBase64)\n\n const byteRate = (sampleRate * channels * bitsPerSample) / 8\n const blockAlign = (channels * bitsPerSample) / 8\n const dataSize = pcmBytes.byteLength\n const buffer = new ArrayBuffer(44 + dataSize)\n const view = new DataView(buffer)\n\n writeAscii(view, 0, 'RIFF')\n view.setUint32(4, 36 + dataSize, true)\n writeAscii(view, 8, 'WAVE')\n writeAscii(view, 12, 'fmt ')\n view.setUint32(16, 16, true)\n view.setUint16(20, 1, true)\n view.setUint16(22, channels, true)\n view.setUint32(24, sampleRate, true)\n view.setUint32(28, byteRate, true)\n view.setUint16(32, blockAlign, true)\n view.setUint16(34, bitsPerSample, true)\n writeAscii(view, 36, 'data')\n view.setUint32(40, dataSize, true)\n new Uint8Array(buffer, 44).set(pcmBytes)\n\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(buffer).toString('base64')\n }\n let binary = ''\n const bytes = new Uint8Array(buffer)\n for (const byte of bytes) {\n binary += String.fromCharCode(byte)\n }\n return btoa(binary)\n}\n\nfunction decodeBase64(b64: string): Uint8Array {\n const binary = atob(b64)\n const out = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i)\n return out\n}\n\nfunction writeAscii(view: DataView, offset: number, text: string): void {\n for (let i = 0; i < text.length; i += 1) {\n view.setUint8(offset + i, text.charCodeAt(i))\n }\n}\n\n/**\n * Creates a Gemini TTS adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini TTS adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiSpeech('gemini-2.5-flash-preview-tts', \"your-api-key\");\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello, world!'\n * });\n * ```\n */\nexport function createGeminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n // Put apiKey LAST so caller-supplied config can't silently override the\n // explicit argument.\n return new GeminiTTSAdapter({ ...config, apiKey }, model)\n}\n\n/**\n * Creates a Gemini speech adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini speech adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiSpeech('gemini-2.5-flash-preview-tts');\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Welcome to TanStack AI!'\n * });\n * ```\n */\nexport function geminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiSpeech(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -23,3 +23,4 @@ export type { GeminiChatModelProviderOptionsByName, GeminiChatModelToolCapabilit
|
|
|
23
23
|
export type { GeminiStructuredOutputOptions, GeminiThinkingOptions, } from './text/text-provider-options.js';
|
|
24
24
|
export type { GoogleGeminiTool } from './tools/index.js';
|
|
25
25
|
export type { GeminiTextMetadata, GeminiImageMetadata, GeminiAudioMetadata, GeminiVideoMetadata, GeminiDocumentMetadata, GeminiMessageMetadataByModality, } from './message-types.js';
|
|
26
|
+
export type { GeminiProviderUsageDetails } from './usage.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { TokenUsage } from '@tanstack/ai';
|
|
2
|
+
import { GenerateContentResponseUsageMetadata, ModalityTokenCount } from '@google/genai';
|
|
3
|
+
/**
|
|
4
|
+
* Flattened modality token counts for normalized usage reporting.
|
|
5
|
+
* Maps Gemini's ModalityTokenCount array to individual fields.
|
|
6
|
+
*/
|
|
7
|
+
export interface FlattenedModalityTokens {
|
|
8
|
+
/** Text tokens */
|
|
9
|
+
textTokens?: number;
|
|
10
|
+
/** Image tokens */
|
|
11
|
+
imageTokens?: number;
|
|
12
|
+
/** Audio tokens */
|
|
13
|
+
audioTokens?: number;
|
|
14
|
+
/** Video tokens */
|
|
15
|
+
videoTokens?: number;
|
|
16
|
+
/** Document tokens (e.g. PDF inputs) */
|
|
17
|
+
documentTokens?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Flattens Gemini's ModalityTokenCount array into individual token fields.
|
|
21
|
+
* Extracts TEXT, IMAGE, AUDIO, VIDEO, DOCUMENT modality counts into a
|
|
22
|
+
* normalized structure.
|
|
23
|
+
*/
|
|
24
|
+
export declare function flattenModalityTokenCounts(modalities?: Array<ModalityTokenCount>): FlattenedModalityTokens;
|
|
25
|
+
/**
|
|
26
|
+
* Checks if a FlattenedModalityTokens object has any values set.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hasModalityTokens(tokens: FlattenedModalityTokens): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Gemini-specific provider usage details.
|
|
31
|
+
* These fields are unique to Gemini and placed in providerUsageDetails.
|
|
32
|
+
*/
|
|
33
|
+
export type GeminiProviderUsageDetails = {
|
|
34
|
+
/**
|
|
35
|
+
* The traffic type for this request.
|
|
36
|
+
* Can indicate whether request was handled by different service tiers.
|
|
37
|
+
*/
|
|
38
|
+
trafficType?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Number of tokens in the results from tool executions,
|
|
41
|
+
* which are provided back to the model as input.
|
|
42
|
+
*/
|
|
43
|
+
toolUsePromptTokenCount?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Detailed breakdown by modality of the token counts from
|
|
46
|
+
* the results of tool executions.
|
|
47
|
+
*/
|
|
48
|
+
toolUsePromptTokensDetails?: Array<{
|
|
49
|
+
modality: string;
|
|
50
|
+
tokenCount: number;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Detailed breakdown of cache tokens by modality.
|
|
54
|
+
* More granular than the normalized cachedTokens field.
|
|
55
|
+
*/
|
|
56
|
+
cacheTokensDetails?: Array<{
|
|
57
|
+
modality: string;
|
|
58
|
+
tokenCount: number;
|
|
59
|
+
}>;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Build normalized TokenUsage from Gemini's usageMetadata.
|
|
63
|
+
* Handles modality breakdowns and thinking tokens. Returns `undefined` when the
|
|
64
|
+
* provider reported no usage metadata, so callers omit the field rather than
|
|
65
|
+
* fabricating zeroed totals.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildGeminiUsage(usageMetadata: GenerateContentResponseUsageMetadata | undefined | null): TokenUsage<GeminiProviderUsageDetails> | undefined;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { buildBaseUsage } from "@tanstack/ai";
|
|
2
|
+
function flattenModalityTokenCounts(modalities) {
|
|
3
|
+
if (!modalities || modalities.length === 0) {
|
|
4
|
+
return {};
|
|
5
|
+
}
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const item of modalities) {
|
|
8
|
+
if (!item.modality || item.tokenCount === void 0) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const modality = item.modality.toUpperCase();
|
|
12
|
+
const count = item.tokenCount;
|
|
13
|
+
switch (modality) {
|
|
14
|
+
case "TEXT":
|
|
15
|
+
result.textTokens = (result.textTokens ?? 0) + count;
|
|
16
|
+
break;
|
|
17
|
+
case "IMAGE":
|
|
18
|
+
result.imageTokens = (result.imageTokens ?? 0) + count;
|
|
19
|
+
break;
|
|
20
|
+
case "AUDIO":
|
|
21
|
+
result.audioTokens = (result.audioTokens ?? 0) + count;
|
|
22
|
+
break;
|
|
23
|
+
case "VIDEO":
|
|
24
|
+
result.videoTokens = (result.videoTokens ?? 0) + count;
|
|
25
|
+
break;
|
|
26
|
+
case "DOCUMENT":
|
|
27
|
+
result.documentTokens = (result.documentTokens ?? 0) + count;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function hasModalityTokens(tokens) {
|
|
34
|
+
return tokens.textTokens !== void 0 || tokens.imageTokens !== void 0 || tokens.audioTokens !== void 0 || tokens.videoTokens !== void 0 || tokens.documentTokens !== void 0;
|
|
35
|
+
}
|
|
36
|
+
function buildGeminiUsage(usageMetadata) {
|
|
37
|
+
if (!usageMetadata) return void 0;
|
|
38
|
+
const promptTokens = usageMetadata.promptTokenCount ?? 0;
|
|
39
|
+
const completionTokens = usageMetadata.candidatesTokenCount ?? 0;
|
|
40
|
+
const result = buildBaseUsage({
|
|
41
|
+
promptTokens,
|
|
42
|
+
completionTokens,
|
|
43
|
+
totalTokens: usageMetadata.totalTokenCount ?? promptTokens + completionTokens
|
|
44
|
+
});
|
|
45
|
+
const promptModalities = flattenModalityTokenCounts(
|
|
46
|
+
usageMetadata.promptTokensDetails
|
|
47
|
+
);
|
|
48
|
+
const cachedTokens = usageMetadata.cachedContentTokenCount;
|
|
49
|
+
const promptTokensDetails = {
|
|
50
|
+
...hasModalityTokens(promptModalities) ? promptModalities : {},
|
|
51
|
+
...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {}
|
|
52
|
+
};
|
|
53
|
+
const completionModalities = flattenModalityTokenCounts(
|
|
54
|
+
usageMetadata.candidatesTokensDetails
|
|
55
|
+
);
|
|
56
|
+
const thoughtsTokens = usageMetadata.thoughtsTokenCount;
|
|
57
|
+
const completionTokensDetails = {
|
|
58
|
+
...hasModalityTokens(completionModalities) ? completionModalities : {},
|
|
59
|
+
// Map thoughtsTokenCount to reasoningTokens for consistency with OpenAI
|
|
60
|
+
...thoughtsTokens !== void 0 && thoughtsTokens > 0 ? { reasoningTokens: thoughtsTokens } : {}
|
|
61
|
+
};
|
|
62
|
+
const providerDetails = {
|
|
63
|
+
...usageMetadata.trafficType ? { trafficType: usageMetadata.trafficType } : {},
|
|
64
|
+
...usageMetadata.toolUsePromptTokenCount !== void 0 && usageMetadata.toolUsePromptTokenCount > 0 ? { toolUsePromptTokenCount: usageMetadata.toolUsePromptTokenCount } : {},
|
|
65
|
+
...usageMetadata.toolUsePromptTokensDetails && usageMetadata.toolUsePromptTokensDetails.length > 0 ? {
|
|
66
|
+
toolUsePromptTokensDetails: usageMetadata.toolUsePromptTokensDetails.map((item) => ({
|
|
67
|
+
modality: item.modality || "UNKNOWN",
|
|
68
|
+
tokenCount: item.tokenCount ?? 0
|
|
69
|
+
}))
|
|
70
|
+
} : {},
|
|
71
|
+
...usageMetadata.cacheTokensDetails && usageMetadata.cacheTokensDetails.length > 0 ? {
|
|
72
|
+
cacheTokensDetails: usageMetadata.cacheTokensDetails.map((item) => ({
|
|
73
|
+
modality: item.modality || "UNKNOWN",
|
|
74
|
+
tokenCount: item.tokenCount ?? 0
|
|
75
|
+
}))
|
|
76
|
+
} : {}
|
|
77
|
+
};
|
|
78
|
+
if (Object.keys(promptTokensDetails).length > 0) {
|
|
79
|
+
result.promptTokensDetails = promptTokensDetails;
|
|
80
|
+
}
|
|
81
|
+
if (Object.keys(providerDetails).length > 0) {
|
|
82
|
+
result.providerUsageDetails = providerDetails;
|
|
83
|
+
}
|
|
84
|
+
if (Object.keys(completionTokensDetails).length > 0) {
|
|
85
|
+
result.completionTokensDetails = completionTokensDetails;
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
buildGeminiUsage,
|
|
91
|
+
flattenModalityTokenCounts,
|
|
92
|
+
hasModalityTokens
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.js","sources":["../../src/usage.ts"],"sourcesContent":["import { buildBaseUsage } from '@tanstack/ai'\nimport type { TokenUsage } from '@tanstack/ai'\nimport type {\n GenerateContentResponseUsageMetadata,\n ModalityTokenCount,\n} from '@google/genai'\n\n/**\n * Flattened modality token counts for normalized usage reporting.\n * Maps Gemini's ModalityTokenCount array to individual fields.\n */\nexport interface FlattenedModalityTokens {\n /** Text tokens */\n textTokens?: number\n /** Image tokens */\n imageTokens?: number\n /** Audio tokens */\n audioTokens?: number\n /** Video tokens */\n videoTokens?: number\n /** Document tokens (e.g. PDF inputs) */\n documentTokens?: number\n}\n\n/**\n * Flattens Gemini's ModalityTokenCount array into individual token fields.\n * Extracts TEXT, IMAGE, AUDIO, VIDEO, DOCUMENT modality counts into a\n * normalized structure.\n */\nexport function flattenModalityTokenCounts(\n modalities?: Array<ModalityTokenCount>,\n): FlattenedModalityTokens {\n if (!modalities || modalities.length === 0) {\n return {}\n }\n\n const result: FlattenedModalityTokens = {}\n\n for (const item of modalities) {\n if (!item.modality || item.tokenCount === undefined) {\n continue\n }\n\n const modality = item.modality.toUpperCase()\n const count = item.tokenCount\n\n switch (modality) {\n case 'TEXT':\n result.textTokens = (result.textTokens ?? 0) + count\n break\n case 'IMAGE':\n result.imageTokens = (result.imageTokens ?? 0) + count\n break\n case 'AUDIO':\n result.audioTokens = (result.audioTokens ?? 0) + count\n break\n case 'VIDEO':\n result.videoTokens = (result.videoTokens ?? 0) + count\n break\n case 'DOCUMENT':\n result.documentTokens = (result.documentTokens ?? 0) + count\n break\n }\n }\n\n return result\n}\n\n/**\n * Checks if a FlattenedModalityTokens object has any values set.\n */\nexport function hasModalityTokens(tokens: FlattenedModalityTokens): boolean {\n return (\n tokens.textTokens !== undefined ||\n tokens.imageTokens !== undefined ||\n tokens.audioTokens !== undefined ||\n tokens.videoTokens !== undefined ||\n tokens.documentTokens !== undefined\n )\n}\n\n/**\n * Gemini-specific provider usage details.\n * These fields are unique to Gemini and placed in providerUsageDetails.\n */\nexport type GeminiProviderUsageDetails = {\n /**\n * The traffic type for this request.\n * Can indicate whether request was handled by different service tiers.\n */\n trafficType?: string\n /**\n * Number of tokens in the results from tool executions,\n * which are provided back to the model as input.\n */\n toolUsePromptTokenCount?: number\n /**\n * Detailed breakdown by modality of the token counts from\n * the results of tool executions.\n */\n toolUsePromptTokensDetails?: Array<{\n modality: string\n tokenCount: number\n }>\n /**\n * Detailed breakdown of cache tokens by modality.\n * More granular than the normalized cachedTokens field.\n */\n cacheTokensDetails?: Array<{\n modality: string\n tokenCount: number\n }>\n}\n\n/**\n * Build normalized TokenUsage from Gemini's usageMetadata.\n * Handles modality breakdowns and thinking tokens. Returns `undefined` when the\n * provider reported no usage metadata, so callers omit the field rather than\n * fabricating zeroed totals.\n */\nexport function buildGeminiUsage(\n usageMetadata: GenerateContentResponseUsageMetadata | undefined | null,\n): TokenUsage<GeminiProviderUsageDetails> | undefined {\n if (!usageMetadata) return undefined\n\n const promptTokens = usageMetadata.promptTokenCount ?? 0\n const completionTokens = usageMetadata.candidatesTokenCount ?? 0\n\n const result = buildBaseUsage<GeminiProviderUsageDetails>({\n promptTokens: promptTokens,\n completionTokens: completionTokens,\n totalTokens:\n usageMetadata.totalTokenCount ?? promptTokens + completionTokens,\n })\n\n // Add prompt token details\n // Flatten modality breakdown for prompt\n const promptModalities = flattenModalityTokenCounts(\n usageMetadata.promptTokensDetails,\n )\n const cachedTokens = usageMetadata.cachedContentTokenCount\n\n const promptTokensDetails = {\n ...(hasModalityTokens(promptModalities) ? promptModalities : {}),\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n }\n\n // Add completion token details\n // Flatten modality breakdown for candidates (output)\n const completionModalities = flattenModalityTokenCounts(\n usageMetadata.candidatesTokensDetails,\n )\n const thoughtsTokens = usageMetadata.thoughtsTokenCount\n\n const completionTokensDetails = {\n ...(hasModalityTokens(completionModalities) ? completionModalities : {}),\n // Map thoughtsTokenCount to reasoningTokens for consistency with OpenAI\n ...(thoughtsTokens !== undefined && thoughtsTokens > 0\n ? { reasoningTokens: thoughtsTokens }\n : {}),\n }\n\n // Add provider-specific details\n const providerDetails: GeminiProviderUsageDetails = {\n ...(usageMetadata.trafficType\n ? { trafficType: usageMetadata.trafficType }\n : {}),\n ...(usageMetadata.toolUsePromptTokenCount !== undefined &&\n usageMetadata.toolUsePromptTokenCount > 0\n ? { toolUsePromptTokenCount: usageMetadata.toolUsePromptTokenCount }\n : {}),\n ...(usageMetadata.toolUsePromptTokensDetails &&\n usageMetadata.toolUsePromptTokensDetails.length > 0\n ? {\n toolUsePromptTokensDetails:\n usageMetadata.toolUsePromptTokensDetails.map((item) => ({\n modality: item.modality || 'UNKNOWN',\n tokenCount: item.tokenCount ?? 0,\n })),\n }\n : {}),\n ...(usageMetadata.cacheTokensDetails &&\n usageMetadata.cacheTokensDetails.length > 0\n ? {\n cacheTokensDetails: usageMetadata.cacheTokensDetails.map((item) => ({\n modality: item.modality || 'UNKNOWN',\n tokenCount: item.tokenCount ?? 0,\n })),\n }\n : {}),\n }\n\n // Add prompt token details if available\n if (Object.keys(promptTokensDetails).length > 0) {\n result.promptTokensDetails = promptTokensDetails\n }\n // Add provider details if available\n if (Object.keys(providerDetails).length > 0) {\n result.providerUsageDetails = providerDetails\n }\n // Add completion token details if available\n if (Object.keys(completionTokensDetails).length > 0) {\n result.completionTokensDetails = completionTokensDetails\n }\n\n return result\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-gemini",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Google Gemini adapter for TanStack AI chat, images, speech, audio generation, and structured outputs.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
"@tanstack/ai-utils": "0.2.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@tanstack/ai": "^0.
|
|
53
|
+
"@tanstack/ai": "^0.26.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@vitest/coverage-v8": "4.0.14",
|
|
57
57
|
"vite": "^7.3.3",
|
|
58
58
|
"zod": "^4.2.0",
|
|
59
|
-
"@tanstack/ai": "0.
|
|
59
|
+
"@tanstack/ai": "0.26.0"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "vite build",
|
package/src/adapters/audio.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
generateId,
|
|
5
5
|
getGeminiApiKeyFromEnv,
|
|
6
6
|
} from '../utils'
|
|
7
|
+
import { buildGeminiUsage } from '../usage'
|
|
7
8
|
import type { GEMINI_AUDIO_MODELS } from '../model-meta'
|
|
8
9
|
import type {
|
|
9
10
|
AudioGenerationOptions,
|
|
@@ -122,6 +123,11 @@ export class GeminiAudioAdapter<
|
|
|
122
123
|
b64Json: audioPart.inlineData.data,
|
|
123
124
|
...(contentType !== undefined && { contentType }),
|
|
124
125
|
},
|
|
126
|
+
// Surface token usage (with per-modality breakdown) when Gemini reports
|
|
127
|
+
// it. Spread conditionally for exactOptionalPropertyTypes.
|
|
128
|
+
...(response.usageMetadata
|
|
129
|
+
? { usage: buildGeminiUsage(response.usageMetadata) }
|
|
130
|
+
: {}),
|
|
125
131
|
}
|
|
126
132
|
} catch (error) {
|
|
127
133
|
logger.errors('gemini.generateAudio fatal', {
|
package/src/adapters/image.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
generateId,
|
|
5
5
|
getGeminiApiKeyFromEnv,
|
|
6
6
|
} from '../utils'
|
|
7
|
+
import { buildGeminiUsage } from '../usage'
|
|
7
8
|
import {
|
|
8
9
|
parseNativeImageSize,
|
|
9
10
|
sizeToAspectRatio,
|
|
@@ -214,16 +215,13 @@ export class GeminiImageAdapter<
|
|
|
214
215
|
id: generateId(this.name),
|
|
215
216
|
model,
|
|
216
217
|
images,
|
|
217
|
-
// Surface token usage when the model
|
|
218
|
-
// generateContent). Conditionally spread
|
|
219
|
-
// exactOptionalPropertyTypes — only include usage when
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
totalTokens: response.usageMetadata.totalTokenCount ?? 0,
|
|
225
|
-
},
|
|
226
|
-
}),
|
|
218
|
+
// Surface token usage (with per-modality breakdown) when the model
|
|
219
|
+
// reports it (e.g. Nano Banana via generateContent). Conditionally spread
|
|
220
|
+
// to satisfy exactOptionalPropertyTypes — only include usage when
|
|
221
|
+
// present. See #330.
|
|
222
|
+
...(response.usageMetadata
|
|
223
|
+
? { usage: buildGeminiUsage(response.usageMetadata) }
|
|
224
|
+
: {}),
|
|
227
225
|
}
|
|
228
226
|
}
|
|
229
227
|
|
package/src/adapters/text.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { EventType, normalizeSystemPrompts } from '@tanstack/ai'
|
|
|
3
3
|
import { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'
|
|
4
4
|
import { BaseTextAdapter } from '@tanstack/ai/adapters'
|
|
5
5
|
import { convertToolsToProviderFormat } from '../tools/tool-converter'
|
|
6
|
+
import { buildGeminiUsage } from '../usage'
|
|
6
7
|
import {
|
|
7
8
|
createGeminiClient,
|
|
8
9
|
generateId,
|
|
@@ -204,6 +205,9 @@ export class GeminiTextAdapter<
|
|
|
204
205
|
return {
|
|
205
206
|
data: parsed,
|
|
206
207
|
rawText,
|
|
208
|
+
usage: result.usageMetadata
|
|
209
|
+
? buildGeminiUsage(result.usageMetadata)
|
|
210
|
+
: undefined,
|
|
207
211
|
}
|
|
208
212
|
} catch (error) {
|
|
209
213
|
logger.errors('gemini.structuredOutput fatal', {
|
|
@@ -633,11 +637,7 @@ export class GeminiTextAdapter<
|
|
|
633
637
|
// exactOptionalPropertyTypes; only include it when usageMetadata is
|
|
634
638
|
// present rather than assigning an explicit `undefined`.
|
|
635
639
|
...(chunk.usageMetadata && {
|
|
636
|
-
usage:
|
|
637
|
-
promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,
|
|
638
|
-
completionTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
|
|
639
|
-
totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,
|
|
640
|
-
},
|
|
640
|
+
usage: buildGeminiUsage(chunk.usageMetadata),
|
|
641
641
|
}),
|
|
642
642
|
}
|
|
643
643
|
}
|
package/src/adapters/tts.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getGeminiApiKeyFromEnv,
|
|
6
6
|
} from '../utils'
|
|
7
7
|
import { GEMINI_TTS_VOICES } from '../model-meta'
|
|
8
|
+
import { buildGeminiUsage } from '../usage'
|
|
8
9
|
import type { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta'
|
|
9
10
|
import type { TTSOptions, TTSResult } from '@tanstack/ai'
|
|
10
11
|
import type { GoogleGenAI, SpeechConfig } from '@google/genai'
|
|
@@ -229,6 +230,13 @@ export class GeminiTTSAdapter<
|
|
|
229
230
|
// mime is guaranteed by the `startsWith('audio/')` find predicate above.
|
|
230
231
|
const mimeType = audioPart.inlineData.mimeType as string
|
|
231
232
|
|
|
233
|
+
// Surface token usage (with per-modality breakdown) when Gemini reports
|
|
234
|
+
// it. Spread conditionally for exactOptionalPropertyTypes — shared by both
|
|
235
|
+
// the PCM→WAV and pass-through return paths below.
|
|
236
|
+
const usageField = response.usageMetadata
|
|
237
|
+
? { usage: buildGeminiUsage(response.usageMetadata) }
|
|
238
|
+
: {}
|
|
239
|
+
|
|
232
240
|
// Gemini TTS models return raw 16-bit LE PCM with a mime type like
|
|
233
241
|
// `audio/L16;codec=pcm;rate=24000`. That isn't playable in an <audio>
|
|
234
242
|
// element and the bare string isn't a usable file extension, so we
|
|
@@ -247,6 +255,7 @@ export class GeminiTTSAdapter<
|
|
|
247
255
|
audio: wavBase64,
|
|
248
256
|
format: 'wav',
|
|
249
257
|
contentType: 'audio/wav',
|
|
258
|
+
...usageField,
|
|
250
259
|
}
|
|
251
260
|
}
|
|
252
261
|
|
|
@@ -261,6 +270,7 @@ export class GeminiTTSAdapter<
|
|
|
261
270
|
audio: audioBase64,
|
|
262
271
|
format,
|
|
263
272
|
contentType: mimeType,
|
|
273
|
+
...usageField,
|
|
264
274
|
}
|
|
265
275
|
} catch (error) {
|
|
266
276
|
logger.errors('gemini.generateSpeech fatal', {
|
package/src/index.ts
CHANGED
package/src/usage.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { buildBaseUsage } from '@tanstack/ai'
|
|
2
|
+
import type { TokenUsage } from '@tanstack/ai'
|
|
3
|
+
import type {
|
|
4
|
+
GenerateContentResponseUsageMetadata,
|
|
5
|
+
ModalityTokenCount,
|
|
6
|
+
} from '@google/genai'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Flattened modality token counts for normalized usage reporting.
|
|
10
|
+
* Maps Gemini's ModalityTokenCount array to individual fields.
|
|
11
|
+
*/
|
|
12
|
+
export interface FlattenedModalityTokens {
|
|
13
|
+
/** Text tokens */
|
|
14
|
+
textTokens?: number
|
|
15
|
+
/** Image tokens */
|
|
16
|
+
imageTokens?: number
|
|
17
|
+
/** Audio tokens */
|
|
18
|
+
audioTokens?: number
|
|
19
|
+
/** Video tokens */
|
|
20
|
+
videoTokens?: number
|
|
21
|
+
/** Document tokens (e.g. PDF inputs) */
|
|
22
|
+
documentTokens?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Flattens Gemini's ModalityTokenCount array into individual token fields.
|
|
27
|
+
* Extracts TEXT, IMAGE, AUDIO, VIDEO, DOCUMENT modality counts into a
|
|
28
|
+
* normalized structure.
|
|
29
|
+
*/
|
|
30
|
+
export function flattenModalityTokenCounts(
|
|
31
|
+
modalities?: Array<ModalityTokenCount>,
|
|
32
|
+
): FlattenedModalityTokens {
|
|
33
|
+
if (!modalities || modalities.length === 0) {
|
|
34
|
+
return {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result: FlattenedModalityTokens = {}
|
|
38
|
+
|
|
39
|
+
for (const item of modalities) {
|
|
40
|
+
if (!item.modality || item.tokenCount === undefined) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const modality = item.modality.toUpperCase()
|
|
45
|
+
const count = item.tokenCount
|
|
46
|
+
|
|
47
|
+
switch (modality) {
|
|
48
|
+
case 'TEXT':
|
|
49
|
+
result.textTokens = (result.textTokens ?? 0) + count
|
|
50
|
+
break
|
|
51
|
+
case 'IMAGE':
|
|
52
|
+
result.imageTokens = (result.imageTokens ?? 0) + count
|
|
53
|
+
break
|
|
54
|
+
case 'AUDIO':
|
|
55
|
+
result.audioTokens = (result.audioTokens ?? 0) + count
|
|
56
|
+
break
|
|
57
|
+
case 'VIDEO':
|
|
58
|
+
result.videoTokens = (result.videoTokens ?? 0) + count
|
|
59
|
+
break
|
|
60
|
+
case 'DOCUMENT':
|
|
61
|
+
result.documentTokens = (result.documentTokens ?? 0) + count
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Checks if a FlattenedModalityTokens object has any values set.
|
|
71
|
+
*/
|
|
72
|
+
export function hasModalityTokens(tokens: FlattenedModalityTokens): boolean {
|
|
73
|
+
return (
|
|
74
|
+
tokens.textTokens !== undefined ||
|
|
75
|
+
tokens.imageTokens !== undefined ||
|
|
76
|
+
tokens.audioTokens !== undefined ||
|
|
77
|
+
tokens.videoTokens !== undefined ||
|
|
78
|
+
tokens.documentTokens !== undefined
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gemini-specific provider usage details.
|
|
84
|
+
* These fields are unique to Gemini and placed in providerUsageDetails.
|
|
85
|
+
*/
|
|
86
|
+
export type GeminiProviderUsageDetails = {
|
|
87
|
+
/**
|
|
88
|
+
* The traffic type for this request.
|
|
89
|
+
* Can indicate whether request was handled by different service tiers.
|
|
90
|
+
*/
|
|
91
|
+
trafficType?: string
|
|
92
|
+
/**
|
|
93
|
+
* Number of tokens in the results from tool executions,
|
|
94
|
+
* which are provided back to the model as input.
|
|
95
|
+
*/
|
|
96
|
+
toolUsePromptTokenCount?: number
|
|
97
|
+
/**
|
|
98
|
+
* Detailed breakdown by modality of the token counts from
|
|
99
|
+
* the results of tool executions.
|
|
100
|
+
*/
|
|
101
|
+
toolUsePromptTokensDetails?: Array<{
|
|
102
|
+
modality: string
|
|
103
|
+
tokenCount: number
|
|
104
|
+
}>
|
|
105
|
+
/**
|
|
106
|
+
* Detailed breakdown of cache tokens by modality.
|
|
107
|
+
* More granular than the normalized cachedTokens field.
|
|
108
|
+
*/
|
|
109
|
+
cacheTokensDetails?: Array<{
|
|
110
|
+
modality: string
|
|
111
|
+
tokenCount: number
|
|
112
|
+
}>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build normalized TokenUsage from Gemini's usageMetadata.
|
|
117
|
+
* Handles modality breakdowns and thinking tokens. Returns `undefined` when the
|
|
118
|
+
* provider reported no usage metadata, so callers omit the field rather than
|
|
119
|
+
* fabricating zeroed totals.
|
|
120
|
+
*/
|
|
121
|
+
export function buildGeminiUsage(
|
|
122
|
+
usageMetadata: GenerateContentResponseUsageMetadata | undefined | null,
|
|
123
|
+
): TokenUsage<GeminiProviderUsageDetails> | undefined {
|
|
124
|
+
if (!usageMetadata) return undefined
|
|
125
|
+
|
|
126
|
+
const promptTokens = usageMetadata.promptTokenCount ?? 0
|
|
127
|
+
const completionTokens = usageMetadata.candidatesTokenCount ?? 0
|
|
128
|
+
|
|
129
|
+
const result = buildBaseUsage<GeminiProviderUsageDetails>({
|
|
130
|
+
promptTokens: promptTokens,
|
|
131
|
+
completionTokens: completionTokens,
|
|
132
|
+
totalTokens:
|
|
133
|
+
usageMetadata.totalTokenCount ?? promptTokens + completionTokens,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Add prompt token details
|
|
137
|
+
// Flatten modality breakdown for prompt
|
|
138
|
+
const promptModalities = flattenModalityTokenCounts(
|
|
139
|
+
usageMetadata.promptTokensDetails,
|
|
140
|
+
)
|
|
141
|
+
const cachedTokens = usageMetadata.cachedContentTokenCount
|
|
142
|
+
|
|
143
|
+
const promptTokensDetails = {
|
|
144
|
+
...(hasModalityTokens(promptModalities) ? promptModalities : {}),
|
|
145
|
+
...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add completion token details
|
|
149
|
+
// Flatten modality breakdown for candidates (output)
|
|
150
|
+
const completionModalities = flattenModalityTokenCounts(
|
|
151
|
+
usageMetadata.candidatesTokensDetails,
|
|
152
|
+
)
|
|
153
|
+
const thoughtsTokens = usageMetadata.thoughtsTokenCount
|
|
154
|
+
|
|
155
|
+
const completionTokensDetails = {
|
|
156
|
+
...(hasModalityTokens(completionModalities) ? completionModalities : {}),
|
|
157
|
+
// Map thoughtsTokenCount to reasoningTokens for consistency with OpenAI
|
|
158
|
+
...(thoughtsTokens !== undefined && thoughtsTokens > 0
|
|
159
|
+
? { reasoningTokens: thoughtsTokens }
|
|
160
|
+
: {}),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Add provider-specific details
|
|
164
|
+
const providerDetails: GeminiProviderUsageDetails = {
|
|
165
|
+
...(usageMetadata.trafficType
|
|
166
|
+
? { trafficType: usageMetadata.trafficType }
|
|
167
|
+
: {}),
|
|
168
|
+
...(usageMetadata.toolUsePromptTokenCount !== undefined &&
|
|
169
|
+
usageMetadata.toolUsePromptTokenCount > 0
|
|
170
|
+
? { toolUsePromptTokenCount: usageMetadata.toolUsePromptTokenCount }
|
|
171
|
+
: {}),
|
|
172
|
+
...(usageMetadata.toolUsePromptTokensDetails &&
|
|
173
|
+
usageMetadata.toolUsePromptTokensDetails.length > 0
|
|
174
|
+
? {
|
|
175
|
+
toolUsePromptTokensDetails:
|
|
176
|
+
usageMetadata.toolUsePromptTokensDetails.map((item) => ({
|
|
177
|
+
modality: item.modality || 'UNKNOWN',
|
|
178
|
+
tokenCount: item.tokenCount ?? 0,
|
|
179
|
+
})),
|
|
180
|
+
}
|
|
181
|
+
: {}),
|
|
182
|
+
...(usageMetadata.cacheTokensDetails &&
|
|
183
|
+
usageMetadata.cacheTokensDetails.length > 0
|
|
184
|
+
? {
|
|
185
|
+
cacheTokensDetails: usageMetadata.cacheTokensDetails.map((item) => ({
|
|
186
|
+
modality: item.modality || 'UNKNOWN',
|
|
187
|
+
tokenCount: item.tokenCount ?? 0,
|
|
188
|
+
})),
|
|
189
|
+
}
|
|
190
|
+
: {}),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add prompt token details if available
|
|
194
|
+
if (Object.keys(promptTokensDetails).length > 0) {
|
|
195
|
+
result.promptTokensDetails = promptTokensDetails
|
|
196
|
+
}
|
|
197
|
+
// Add provider details if available
|
|
198
|
+
if (Object.keys(providerDetails).length > 0) {
|
|
199
|
+
result.providerUsageDetails = providerDetails
|
|
200
|
+
}
|
|
201
|
+
// Add completion token details if available
|
|
202
|
+
if (Object.keys(completionTokensDetails).length > 0) {
|
|
203
|
+
result.completionTokensDetails = completionTokensDetails
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result
|
|
207
|
+
}
|