@tanstack/ai-gemini 0.11.0 → 0.12.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.
@@ -93,7 +93,17 @@ class GeminiImageAdapter extends BaseImageAdapter {
93
93
  return {
94
94
  id: generateId(this.name),
95
95
  model,
96
- images
96
+ images,
97
+ // Surface token usage when the model reports it (e.g. Nano Banana via
98
+ // generateContent). Conditionally spread to satisfy
99
+ // exactOptionalPropertyTypes — only include usage when present. See #330.
100
+ ...response.usageMetadata && {
101
+ usage: {
102
+ inputTokens: response.usageMetadata.promptTokenCount ?? 0,
103
+ outputTokens: response.usageMetadata.candidatesTokenCount ?? 0,
104
+ totalTokens: response.usageMetadata.totalTokenCount ?? 0
105
+ }
106
+ }
97
107
  };
98
108
  }
99
109
  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 }\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 {\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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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @experimental Gemini's Interactions API is in Beta per Google. Shapes and
3
+ * behavior may change between minor releases of `@tanstack/ai-gemini`.
4
+ */
5
+ export { GeminiTextInteractionsAdapter, createGeminiTextInteractions, geminiTextInteractions, type GeminiTextInteractionsConfig, type GeminiTextInteractionsProviderOptions, type GeminiInteractionsStream, } from './text-interactions/adapter.js';
6
+ export type { GeminiInteractionsCustomEvent, GeminiInteractionsCustomEventName, GeminiInteractionsCustomEventValue, } from './text-interactions/events.js';
@@ -0,0 +1,7 @@
1
+ import { GeminiTextInteractionsAdapter, createGeminiTextInteractions, geminiTextInteractions } from "./text-interactions/adapter.js";
2
+ export {
3
+ GeminiTextInteractionsAdapter,
4
+ createGeminiTextInteractions,
5
+ geminiTextInteractions
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,75 @@
1
+ import { BaseTextAdapter, StructuredOutputOptions, StructuredOutputResult } from '@tanstack/ai/adapters';
2
+ import { GeminiChatModelToolCapabilitiesByName, GeminiModelInputModalitiesByName, GeminiModels } from '../../model-meta.js';
3
+ import { Modality, StreamChunk, TextOptions } from '@tanstack/ai';
4
+ import { GeminiInteractionsStream } from './events.js';
5
+ import { ExternalTextInteractionsProviderOptions } from './provider-options.js';
6
+ import { GeminiMessageMetadataByModality } from '../../message-types.js';
7
+ import { GeminiClientConfig } from '../../utils.js';
8
+ export type GeminiTextInteractionsConfig = GeminiClientConfig;
9
+ export type GeminiTextInteractionsProviderOptions = ExternalTextInteractionsProviderOptions;
10
+ /**
11
+ * Resolve provider options for a specific model. The Interactions API's
12
+ * request shape is the same across all chat-capable Gemini models — the
13
+ * SDK doesn't expose a per-model param union — so this currently falls
14
+ * through to the flat `GeminiTextInteractionsProviderOptions` for every
15
+ * model. The alias exists for parity with `GeminiTextAdapter`, so a
16
+ * per-model map can be slotted in later without changing the adapter
17
+ * signature.
18
+ */
19
+ type ResolveProviderOptions = GeminiTextInteractionsProviderOptions;
20
+ /**
21
+ * Resolve input modalities for a specific model. Reuses the chat-model
22
+ * modality map from `model-meta.ts`: passing a `document` content block
23
+ * to a model that doesn't support it is a compile error, matching the
24
+ * sibling `GeminiTextAdapter`.
25
+ */
26
+ type ResolveInputModalities<TModel extends string> = TModel extends keyof GeminiModelInputModalitiesByName ? GeminiModelInputModalitiesByName[TModel] : readonly ['text', 'image', 'audio', 'video', 'document'];
27
+ /**
28
+ * Resolve tool capabilities for a specific model. Reuses the chat-model
29
+ * capability map: `google_maps` / `google_search_retrieval` /
30
+ * `mcp_server` are rejected at runtime by `convertToolsToInteractionsFormat`,
31
+ * but per-model gating happens here at compile time.
32
+ */
33
+ type ResolveToolCapabilities<TModel extends string> = TModel extends keyof GeminiChatModelToolCapabilitiesByName ? NonNullable<GeminiChatModelToolCapabilitiesByName[TModel]> : readonly [];
34
+ /**
35
+ * Tree-shakeable adapter for Gemini's stateful Interactions API. Routes
36
+ * through `client.interactions.create` and surfaces the server-assigned
37
+ * `interactionId` via an AG-UI `CUSTOM` event with
38
+ * `name: 'gemini.interactionId'` emitted just before `RUN_FINISHED`; pass
39
+ * that id back on the next turn via `modelOptions.previous_interaction_id`
40
+ * to continue the conversation without resending history.
41
+ *
42
+ * The Interactions API does NOT support stateless multi-turn replay —
43
+ * passing more than one message in `messages` without a
44
+ * `previous_interaction_id` throws. For a chat UI that maintains local
45
+ * history (e.g. `useChat`), see the "Wiring with `useChat`" section of
46
+ * `docs/adapters/gemini.md` for the canonical client/server pattern.
47
+ *
48
+ * Supports user-defined function tools and the built-in tools
49
+ * `google_search`, `code_execution`, `url_context`, `file_search`, and
50
+ * `computer_use`. Built-in tool *activity* for the four search/exec
51
+ * variants is surfaced via `CUSTOM` events
52
+ * (`gemini.googleSearchCall` / `gemini.googleSearchResult` and the
53
+ * corresponding per-tool variants) carrying the raw Interactions delta;
54
+ * see {@link GeminiInteractionsCustomEvent}. `computer_use` is accepted
55
+ * in the request but the Interactions API does not currently stream
56
+ * per-delta CUSTOM events for it. `google_search_retrieval`,
57
+ * `google_maps`, and `mcp_server` are not supported on this adapter.
58
+ *
59
+ * @experimental Interactions API is in Beta per Google; shapes may change.
60
+ * @see https://ai.google.dev/gemini-api/docs/interactions
61
+ */
62
+ export declare class GeminiTextInteractionsAdapter<TModel extends GeminiModels, TProviderOptions extends Record<string, any> = ResolveProviderOptions, TInputModalities extends ReadonlyArray<Modality> = ResolveInputModalities<TModel>, TToolCapabilities extends ReadonlyArray<string> = ResolveToolCapabilities<TModel>> extends BaseTextAdapter<TModel, TProviderOptions, TInputModalities, GeminiMessageMetadataByModality, TToolCapabilities> {
63
+ readonly kind: "text";
64
+ readonly name: "gemini-text-interactions";
65
+ private readonly client;
66
+ private readonly interactionIdByThread;
67
+ constructor(config: GeminiTextInteractionsConfig, model: TModel);
68
+ chatStream(options: TextOptions<GeminiTextInteractionsProviderOptions>): AsyncIterable<StreamChunk>;
69
+ structuredOutput(options: StructuredOutputOptions<GeminiTextInteractionsProviderOptions>): Promise<StructuredOutputResult<unknown>>;
70
+ }
71
+ /** @experimental Interactions API is in Beta. */
72
+ export declare function createGeminiTextInteractions<TModel extends GeminiModels>(model: TModel, apiKey: string, config?: Omit<GeminiTextInteractionsConfig, 'apiKey'>): GeminiTextInteractionsAdapter<TModel, ResolveProviderOptions, ResolveInputModalities<TModel>, ResolveToolCapabilities<TModel>>;
73
+ /** @experimental Interactions API is in Beta. */
74
+ export declare function geminiTextInteractions<TModel extends GeminiModels>(model: TModel, config?: Omit<GeminiTextInteractionsConfig, 'apiKey'>): GeminiTextInteractionsAdapter<TModel, ResolveProviderOptions, ResolveInputModalities<TModel>, ResolveToolCapabilities<TModel>>;
75
+ export type { GeminiInteractionsStream };