@tanstack/ai-gemini 0.16.0 → 0.17.0

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.
@@ -1,6 +1,6 @@
1
1
  import { BaseImageAdapter } from '@tanstack/ai/adapters';
2
2
  import { GEMINI_IMAGE_MODELS } from '../model-meta.js';
3
- import { GeminiImageModelProviderOptionsByName, GeminiImageModelSizeByName, GeminiImageProviderOptions } from '../image/image-provider-options.js';
3
+ import { GeminiImageModelInputModalitiesByName, GeminiImageModelProviderOptionsByName, GeminiImageModelSizeByName, GeminiImageProviderOptions } from '../image/image-provider-options.js';
4
4
  import { ImageGenerationOptions, ImageGenerationResult } from '@tanstack/ai';
5
5
  import { GeminiClientConfig } from '../utils.js';
6
6
  /**
@@ -24,19 +24,32 @@ export type GeminiImageModel = (typeof GEMINI_IMAGE_MODELS)[number];
24
24
  * - Watermark options
25
25
  * - Extended resolution tiers (Nano Banana 2)
26
26
  */
27
- export declare class GeminiImageAdapter<TModel extends GeminiImageModel> extends BaseImageAdapter<TModel, GeminiImageProviderOptions, GeminiImageModelProviderOptionsByName, GeminiImageModelSizeByName> {
27
+ export declare class GeminiImageAdapter<TModel extends GeminiImageModel> extends BaseImageAdapter<TModel, GeminiImageProviderOptions, GeminiImageModelProviderOptionsByName, GeminiImageModelSizeByName, GeminiImageModelInputModalitiesByName> {
28
28
  readonly kind: "image";
29
29
  readonly name: "gemini";
30
30
  '~types': {
31
31
  providerOptions: GeminiImageProviderOptions;
32
32
  modelProviderOptionsByName: GeminiImageModelProviderOptionsByName;
33
33
  modelSizeByName: GeminiImageModelSizeByName;
34
+ modelInputModalitiesByName: GeminiImageModelInputModalitiesByName;
34
35
  };
35
36
  private readonly client;
36
37
  constructor(config: GeminiImageConfig, model: TModel);
37
38
  generateImages(options: ImageGenerationOptions<GeminiImageProviderOptions>): Promise<ImageGenerationResult>;
38
39
  private isGeminiImageModel;
39
40
  private generateWithGeminiApi;
41
+ /**
42
+ * Build the multimodal `contents` payload. Text-only prompts pass through
43
+ * as a plain string (the SDK accepts it directly); prompts with image
44
+ * parts become a single user `Content` whose `parts` mirror the prompt's
45
+ * interleaved order — position is meaningful to Gemini ("not like this
46
+ * *(image)*, more like this *(image)*").
47
+ *
48
+ * The generateContent API has no numberOfImages parameter, so when more
49
+ * than one image is requested a trailing instruction is appended.
50
+ */
51
+ private buildContents;
52
+ private imagePartToGeminiPart;
40
53
  private transformGeminiResponse;
41
54
  private buildImagenConfig;
42
55
  private transformImagenResponse;
@@ -1,4 +1,6 @@
1
+ import { resolveMediaPrompt } from "@tanstack/ai";
1
2
  import { BaseImageAdapter } from "@tanstack/ai/adapters";
3
+ import { arrayBufferToBase64 } from "@tanstack/ai-utils";
2
4
  import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils/client.js";
3
5
  import { buildGeminiUsage } from "../usage.js";
4
6
  import { validatePrompt, validateImageSize, validateNumberOfImages, parseNativeImageSize, sizeToAspectRatio } from "../image/image-provider-options.js";
@@ -11,7 +13,7 @@ class GeminiImageAdapter extends BaseImageAdapter {
11
13
  this.client = createGeminiClient(config);
12
14
  }
13
15
  async generateImages(options) {
14
- const { model, prompt, logger } = options;
16
+ const { model, logger } = options;
15
17
  logger.request(
16
18
  `activity=generateImage provider=gemini model=${this.model}`,
17
19
  {
@@ -20,16 +22,34 @@ class GeminiImageAdapter extends BaseImageAdapter {
20
22
  }
21
23
  );
22
24
  try {
23
- validatePrompt({ prompt, model });
25
+ const resolved = resolveMediaPrompt(options.prompt);
26
+ if (resolved.images.length === 0) {
27
+ validatePrompt({ prompt: resolved.text, model });
28
+ }
29
+ if (resolved.videos.length > 0) {
30
+ throw new Error(
31
+ `${this.name}.generateImages does not support video prompt parts (model: ${model}).`
32
+ );
33
+ }
34
+ if (resolved.audios.length > 0) {
35
+ throw new Error(
36
+ `${this.name}.generateImages does not support audio prompt parts (model: ${model}).`
37
+ );
38
+ }
24
39
  if (this.isGeminiImageModel(model)) {
25
- return await this.generateWithGeminiApi(options);
40
+ return await this.generateWithGeminiApi(options, resolved);
41
+ }
42
+ if (resolved.images.length > 0) {
43
+ throw new Error(
44
+ `${this.name}: model "${model}" (Imagen) does not support image prompt parts. Use a Gemini-native image model (e.g. gemini-2.5-flash-image, "nano-banana") for image-conditioned generation.`
45
+ );
26
46
  }
27
47
  validateImageSize(model, options.size);
28
48
  validateNumberOfImages(model, options.numberOfImages);
29
49
  const config = this.buildImagenConfig(options);
30
50
  const response = await this.client.models.generateImages({
31
51
  model,
32
- prompt,
52
+ prompt: resolved.text,
33
53
  config
34
54
  });
35
55
  return this.transformImagenResponse(model, response);
@@ -44,10 +64,9 @@ class GeminiImageAdapter extends BaseImageAdapter {
44
64
  isGeminiImageModel(model) {
45
65
  return model.startsWith("gemini-");
46
66
  }
47
- async generateWithGeminiApi(options) {
48
- const { model, prompt, size, numberOfImages, modelOptions } = options;
67
+ async generateWithGeminiApi(options, resolved) {
68
+ const { model, size, numberOfImages, modelOptions } = options;
49
69
  const parsedSize = size ? parseNativeImageSize(size) : void 0;
50
- const augmentedPrompt = numberOfImages && numberOfImages > 1 ? `${prompt} Generate ${numberOfImages} distinct images.` : prompt;
51
70
  const nativeConfig = {};
52
71
  if (modelOptions?.seed !== void 0) {
53
72
  nativeConfig.seed = modelOptions.seed;
@@ -69,13 +88,82 @@ class GeminiImageAdapter extends BaseImageAdapter {
69
88
  }
70
89
  }
71
90
  };
91
+ const contents = await this.buildContents(resolved, numberOfImages);
72
92
  const response = await this.client.models.generateContent({
73
93
  model,
74
- contents: augmentedPrompt,
94
+ contents,
75
95
  config
76
96
  });
77
97
  return this.transformGeminiResponse(model, response);
78
98
  }
99
+ /**
100
+ * Build the multimodal `contents` payload. Text-only prompts pass through
101
+ * as a plain string (the SDK accepts it directly); prompts with image
102
+ * parts become a single user `Content` whose `parts` mirror the prompt's
103
+ * interleaved order — position is meaningful to Gemini ("not like this
104
+ * *(image)*, more like this *(image)*").
105
+ *
106
+ * The generateContent API has no numberOfImages parameter, so when more
107
+ * than one image is requested a trailing instruction is appended.
108
+ */
109
+ async buildContents(resolved, numberOfImages) {
110
+ const countInstruction = numberOfImages && numberOfImages > 1 ? `Generate ${numberOfImages} distinct images.` : void 0;
111
+ if (resolved.images.length === 0) {
112
+ return countInstruction ? `${resolved.text} ${countInstruction}` : resolved.text;
113
+ }
114
+ const parts = await Promise.all(
115
+ resolved.parts.map((part) => {
116
+ if (part.type === "text") {
117
+ return Promise.resolve({ text: part.content });
118
+ }
119
+ if (part.type === "image") {
120
+ return this.imagePartToGeminiPart(part);
121
+ }
122
+ throw new Error(
123
+ `gemini: unsupported prompt part type "${part.type}" in image generation.`
124
+ );
125
+ })
126
+ );
127
+ if (countInstruction) {
128
+ parts.push({ text: countInstruction });
129
+ }
130
+ return [{ role: "user", parts }];
131
+ }
132
+ async imagePartToGeminiPart(part) {
133
+ if (part.source.type === "data") {
134
+ return {
135
+ inlineData: {
136
+ mimeType: part.source.mimeType || "image/png",
137
+ data: part.source.value
138
+ }
139
+ };
140
+ }
141
+ if (part.source.value.startsWith("gs://") || /^https?:\/\/generativelanguage\.googleapis\.com\//.test(
142
+ part.source.value
143
+ )) {
144
+ return {
145
+ fileData: {
146
+ fileUri: part.source.value,
147
+ ...part.source.mimeType && { mimeType: part.source.mimeType }
148
+ }
149
+ };
150
+ }
151
+ const response = await fetch(part.source.value);
152
+ if (!response.ok) {
153
+ throw new Error(
154
+ `Failed to fetch image input (${response.status} ${response.statusText}): ${part.source.value}`
155
+ );
156
+ }
157
+ const blob = await response.blob();
158
+ const buffer = await blob.arrayBuffer();
159
+ const base64 = arrayBufferToBase64(buffer);
160
+ return {
161
+ inlineData: {
162
+ mimeType: part.source.mimeType || blob.type || "image/png",
163
+ data: base64
164
+ }
165
+ };
166
+ }
79
167
  transformGeminiResponse(model, response) {
80
168
  const images = [];
81
169
  const textParts = [];
@@ -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 { 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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"}
1
+ {"version":3,"file":"image.js","sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport { arrayBufferToBase64 } from '@tanstack/ai-utils'\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 GeminiImageModelInputModalitiesByName,\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName,\n GeminiImageProviderOptions,\n} from '../image/image-provider-options'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n ImagePart,\n MediaInputMetadata,\n ResolvedMediaPrompt,\n} from '@tanstack/ai'\nimport type {\n Content,\n GenerateContentConfig,\n GenerateContentResponse,\n GenerateImagesConfig,\n GenerateImagesResponse,\n GoogleGenAI,\n Part,\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 GeminiImageModelInputModalitiesByName\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 modelInputModalitiesByName: GeminiImageModelInputModalitiesByName\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, 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 const resolved = resolveMediaPrompt(options.prompt)\n\n // Image-only prompts are allowed (the image inputs carry the intent);\n // a prompt with neither text nor images is always an error.\n if (resolved.images.length === 0) {\n validatePrompt({ prompt: resolved.text, model })\n }\n\n if (resolved.videos.length > 0) {\n throw new Error(\n `${this.name}.generateImages does not support video prompt parts (model: ${model}).`,\n )\n }\n if (resolved.audios.length > 0) {\n throw new Error(\n `${this.name}.generateImages does not support audio prompt parts (model: ${model}).`,\n )\n }\n\n if (this.isGeminiImageModel(model)) {\n return await this.generateWithGeminiApi(options, resolved)\n }\n\n // Imagen does not accept image inputs — it's strictly text-to-image.\n if (resolved.images.length > 0) {\n throw new Error(\n `${this.name}: model \"${model}\" (Imagen) does not support image prompt parts. ` +\n `Use a Gemini-native image model (e.g. gemini-2.5-flash-image, \"nano-banana\") for image-conditioned generation.`,\n )\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: resolved.text,\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 resolved: ResolvedMediaPrompt,\n ): Promise<ImageGenerationResult> {\n const { model, size, numberOfImages, modelOptions } = options\n\n const parsedSize = size ? parseNativeImageSize(size) : undefined\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 contents = await this.buildContents(resolved, numberOfImages)\n\n const response = await this.client.models.generateContent({\n model,\n contents,\n config,\n })\n\n return this.transformGeminiResponse(model, response)\n }\n\n /**\n * Build the multimodal `contents` payload. Text-only prompts pass through\n * as a plain string (the SDK accepts it directly); prompts with image\n * parts become a single user `Content` whose `parts` mirror the prompt's\n * interleaved order — position is meaningful to Gemini (\"not like this\n * *(image)*, more like this *(image)*\").\n *\n * The generateContent API has no numberOfImages parameter, so when more\n * than one image is requested a trailing instruction is appended.\n */\n private async buildContents(\n resolved: ResolvedMediaPrompt,\n numberOfImages: number | undefined,\n ): Promise<string | Array<Content>> {\n const countInstruction =\n numberOfImages && numberOfImages > 1\n ? `Generate ${numberOfImages} distinct images.`\n : undefined\n\n if (resolved.images.length === 0) {\n return countInstruction\n ? `${resolved.text} ${countInstruction}`\n : resolved.text\n }\n\n const parts: Array<Part> = await Promise.all(\n resolved.parts.map((part) => {\n if (part.type === 'text') {\n return Promise.resolve<Part>({ text: part.content })\n }\n if (part.type === 'image') {\n return this.imagePartToGeminiPart(part)\n }\n // Video / audio parts were rejected in generateImages above.\n throw new Error(\n `gemini: unsupported prompt part type \"${part.type}\" in image generation.`,\n )\n }),\n )\n if (countInstruction) {\n parts.push({ text: countInstruction })\n }\n return [{ role: 'user', parts }]\n }\n\n private async imagePartToGeminiPart(\n part: ImagePart<MediaInputMetadata>,\n ): Promise<Part> {\n if (part.source.type === 'data') {\n return {\n inlineData: {\n mimeType: part.source.mimeType || 'image/png',\n data: part.source.value,\n },\n }\n }\n // For URL sources, prefer passing the URL through as `fileData` when it\n // looks like a Google Files API URI; otherwise fetch and inline as base64.\n if (\n part.source.value.startsWith('gs://') ||\n /^https?:\\/\\/generativelanguage\\.googleapis\\.com\\//.test(\n part.source.value,\n )\n ) {\n return {\n fileData: {\n fileUri: part.source.value,\n ...(part.source.mimeType && { mimeType: part.source.mimeType }),\n },\n }\n }\n const response = await fetch(part.source.value)\n if (!response.ok) {\n throw new Error(\n `Failed to fetch image input (${response.status} ${response.statusText}): ${part.source.value}`,\n )\n }\n const blob = await response.blob()\n const buffer = await blob.arrayBuffer()\n const base64 = arrayBufferToBase64(buffer)\n return {\n inlineData: {\n mimeType: part.source.mimeType || blob.type || 'image/png',\n data: base64,\n },\n }\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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"}
@@ -0,0 +1,108 @@
1
+ import { BaseVideoAdapter, DurationOptions } from '@tanstack/ai/adapters';
2
+ import { VideoGenerationOptions, VideoJobResult, VideoStatusResult, VideoUrlResult } from '@tanstack/ai';
3
+ import { GoogleGenAI } from '@google/genai';
4
+ import { GeminiVideoModel, GeminiVideoModelDurationByName, GeminiVideoModelInputModalitiesByName, GeminiVideoModelProviderOptionsByName, GeminiVideoModelSizeByName, GeminiVideoProviderOptions, GeminiVideoSize } from '../video/video-provider-options.js';
5
+ import { GeminiClientConfig } from '../utils.js';
6
+ /**
7
+ * Configuration for Gemini video adapter.
8
+ *
9
+ * @experimental Video generation is an experimental feature and may change.
10
+ */
11
+ export interface GeminiVideoConfig extends GeminiClientConfig {
12
+ }
13
+ /**
14
+ * Gemini Veo Video Generation Adapter
15
+ *
16
+ * Tree-shakeable adapter for Google Veo video generation. Veo runs as a
17
+ * long-running operation: `createVideoJob` starts the operation via the
18
+ * `:predictLongRunning` endpoint, `getVideoStatus` polls it, and
19
+ * `getVideoUrl` extracts the generated video's URI once it completes.
20
+ *
21
+ * Image prompt parts are routed by `metadata.role`:
22
+ * - `'start_frame'` (or the first un-roled image) → the input image the
23
+ * video starts from
24
+ * - `'end_frame'` → `lastFrame` (the frame the video ends on)
25
+ * - `'reference'` / `'character'` → `referenceImages` (asset references,
26
+ * Veo 3.1)
27
+ *
28
+ * Note: the returned video URI is served by the Gemini Files API and
29
+ * requires the API key (`x-goog-api-key` header or `?key=` query
30
+ * parameter) to download.
31
+ *
32
+ * @experimental Video generation is an experimental feature and may change.
33
+ */
34
+ export declare class GeminiVideoAdapter<TModel extends GeminiVideoModel> extends BaseVideoAdapter<TModel, GeminiVideoProviderOptions, GeminiVideoModelProviderOptionsByName, GeminiVideoModelSizeByName, GeminiVideoModelInputModalitiesByName, GeminiVideoModelDurationByName> {
35
+ readonly name: "gemini";
36
+ protected client: GoogleGenAI;
37
+ constructor(config: GeminiVideoConfig, model: TModel);
38
+ createVideoJob(options: VideoGenerationOptions<GeminiVideoProviderOptions, GeminiVideoSize, GeminiVideoModelDurationByName[TModel]>): Promise<VideoJobResult>;
39
+ /**
40
+ * Route image prompt parts onto Veo's request fields by `metadata.role`.
41
+ */
42
+ private routeImageParts;
43
+ getVideoStatus(jobId: string): Promise<VideoStatusResult>;
44
+ getVideoUrl(jobId: string): Promise<VideoUrlResult>;
45
+ availableDurations(): DurationOptions<GeminiVideoModelDurationByName[TModel]>;
46
+ snapDuration(seconds: number): GeminiVideoModelDurationByName[TModel] | undefined;
47
+ /**
48
+ * Fetch the long-running operation by name. The SDK's
49
+ * `operations.getVideosOperation` needs a real `GenerateVideosOperation`
50
+ * instance (it calls `_fromAPIResponse` on it), so reconstruct one from
51
+ * the job ID rather than passing an object literal.
52
+ */
53
+ private getOperation;
54
+ }
55
+ /**
56
+ * Creates a Gemini video adapter with an explicit API key.
57
+ * Type resolution happens here at the call site.
58
+ *
59
+ * @experimental Video generation is an experimental feature and may change.
60
+ *
61
+ * @param model - The model name (e.g., 'veo-3.1-generate-preview')
62
+ * @param apiKey - Your Google API key
63
+ * @param config - Optional additional configuration
64
+ * @returns Configured Gemini video adapter instance with resolved types
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const adapter = createGeminiVideo('veo-3.1-generate-preview', 'your-api-key');
69
+ *
70
+ * const { jobId } = await generateVideo({
71
+ * adapter,
72
+ * prompt: 'A beautiful sunset over the ocean',
73
+ * duration: adapter.snapDuration(7), // → 6
74
+ * });
75
+ * ```
76
+ */
77
+ export declare function createGeminiVideo<TModel extends GeminiVideoModel>(model: TModel, apiKey: string, config?: Omit<GeminiVideoConfig, 'apiKey'>): GeminiVideoAdapter<TModel>;
78
+ /**
79
+ * Creates a Gemini video adapter with automatic API key detection from environment variables.
80
+ * Type resolution happens here at the call site.
81
+ *
82
+ * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:
83
+ * - `process.env` (Node.js)
84
+ * - `window.env` (Browser with injected env)
85
+ *
86
+ * @experimental Video generation is an experimental feature and may change.
87
+ *
88
+ * @param model - The model name (e.g., 'veo-3.1-generate-preview')
89
+ * @param config - Optional configuration (excluding apiKey which is auto-detected)
90
+ * @returns Configured Gemini video adapter instance with resolved types
91
+ * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * // Automatically uses GOOGLE_API_KEY from environment
96
+ * const adapter = geminiVideo('veo-3.1-generate-preview');
97
+ *
98
+ * // Create a video generation job
99
+ * const { jobId } = await generateVideo({
100
+ * adapter,
101
+ * prompt: 'A cat playing piano'
102
+ * });
103
+ *
104
+ * // Poll for status
105
+ * const status = await getVideoJobStatus({ adapter, jobId });
106
+ * ```
107
+ */
108
+ export declare function geminiVideo<TModel extends GeminiVideoModel>(model: TModel, config?: Omit<GeminiVideoConfig, 'apiKey'>): GeminiVideoAdapter<TModel>;
@@ -0,0 +1,227 @@
1
+ import { VideoGenerationReferenceType, GenerateVideosOperation } from "@google/genai";
2
+ import { resolveMediaPrompt } from "@tanstack/ai";
3
+ import { BaseVideoAdapter, snapToDurationOption } from "@tanstack/ai/adapters";
4
+ import { arrayBufferToBase64 } from "@tanstack/ai-utils";
5
+ import { createGeminiClient, getGeminiApiKeyFromEnv } from "../utils/client.js";
6
+ import { getGeminiVideoDurationOptions } from "../video/video-provider-options.js";
7
+ function operationErrorMessage(error) {
8
+ if (typeof error.message === "string" && error.message.length > 0) {
9
+ return error.message;
10
+ }
11
+ return JSON.stringify(error);
12
+ }
13
+ async function imagePartToVeoImage(part) {
14
+ if (part.source.type === "data") {
15
+ return {
16
+ imageBytes: part.source.value,
17
+ mimeType: part.source.mimeType || "image/png"
18
+ };
19
+ }
20
+ const url = part.source.value;
21
+ if (url.startsWith("gs://")) {
22
+ return {
23
+ gcsUri: url,
24
+ ...part.source.mimeType && { mimeType: part.source.mimeType }
25
+ };
26
+ }
27
+ if (url.startsWith("data:")) {
28
+ const match = url.match(/^data:([^;,]+)?(;base64)?,(.*)$/);
29
+ if (!match || !match[2]) {
30
+ throw new Error(
31
+ "gemini: only base64 data: URIs are supported for video image inputs."
32
+ );
33
+ }
34
+ return {
35
+ imageBytes: match[3] ?? "",
36
+ mimeType: match[1] || part.source.mimeType || "image/png"
37
+ };
38
+ }
39
+ const response = await fetch(url);
40
+ if (!response.ok) {
41
+ throw new Error(
42
+ `Failed to fetch image input (${response.status} ${response.statusText}): ${url}`
43
+ );
44
+ }
45
+ const blob = await response.blob();
46
+ const buffer = await blob.arrayBuffer();
47
+ return {
48
+ imageBytes: arrayBufferToBase64(buffer),
49
+ mimeType: part.source.mimeType || blob.type || "image/png"
50
+ };
51
+ }
52
+ class GeminiVideoAdapter extends BaseVideoAdapter {
53
+ name = "gemini";
54
+ client;
55
+ constructor(config, model) {
56
+ super({}, model);
57
+ this.client = createGeminiClient(config);
58
+ }
59
+ async createVideoJob(options) {
60
+ const { prompt, size, duration, modelOptions, logger } = options;
61
+ logger.request(
62
+ `activity=video.create provider=${this.name} model=${this.model} size=${size ?? "default"} duration=${duration ?? "default"}`,
63
+ { provider: this.name, model: this.model }
64
+ );
65
+ try {
66
+ const resolved = resolveMediaPrompt(prompt);
67
+ if (resolved.videos.length > 0) {
68
+ throw new Error(
69
+ `${this.name}.createVideoJob does not support video prompt parts (model: ${this.model}).`
70
+ );
71
+ }
72
+ if (resolved.audios.length > 0) {
73
+ throw new Error(
74
+ `${this.name}.createVideoJob does not support audio prompt parts (model: ${this.model}).`
75
+ );
76
+ }
77
+ const { image, lastFrame, referenceImages } = await this.routeImageParts(
78
+ resolved.images
79
+ );
80
+ const config = {
81
+ ...modelOptions,
82
+ ...size !== void 0 && { aspectRatio: size },
83
+ ...duration !== void 0 && { durationSeconds: duration },
84
+ ...lastFrame && { lastFrame },
85
+ ...referenceImages.length > 0 && { referenceImages }
86
+ };
87
+ const operation = await this.client.models.generateVideos({
88
+ model: this.model,
89
+ prompt: resolved.text,
90
+ ...image && { image },
91
+ config
92
+ });
93
+ if (!operation.name) {
94
+ throw new Error(
95
+ "Veo did not return an operation name for the video generation job."
96
+ );
97
+ }
98
+ return { jobId: operation.name, model: this.model };
99
+ } catch (error) {
100
+ logger.errors(`${this.name}.createVideoJob fatal`, {
101
+ error,
102
+ source: `${this.name}.createVideoJob`
103
+ });
104
+ throw error;
105
+ }
106
+ }
107
+ /**
108
+ * Route image prompt parts onto Veo's request fields by `metadata.role`.
109
+ */
110
+ async routeImageParts(parts) {
111
+ let image;
112
+ let lastFrame;
113
+ const referenceImages = [];
114
+ for (const part of parts) {
115
+ const role = part.metadata?.role;
116
+ switch (role) {
117
+ case "end_frame": {
118
+ if (lastFrame) {
119
+ throw new Error(
120
+ `${this.name}: Veo accepts at most one 'end_frame' image.`
121
+ );
122
+ }
123
+ lastFrame = await imagePartToVeoImage(part);
124
+ break;
125
+ }
126
+ case "reference":
127
+ case "character": {
128
+ referenceImages.push({
129
+ image: await imagePartToVeoImage(part),
130
+ referenceType: VideoGenerationReferenceType.ASSET
131
+ });
132
+ break;
133
+ }
134
+ case "start_frame":
135
+ case void 0: {
136
+ if (image) {
137
+ throw new Error(
138
+ `${this.name}: Veo accepts at most one starting image; received multiple 'start_frame'/un-roled images. Use metadata.role ('end_frame', 'reference') to disambiguate the others.`
139
+ );
140
+ }
141
+ image = await imagePartToVeoImage(part);
142
+ break;
143
+ }
144
+ case "mask":
145
+ case "control":
146
+ throw new Error(
147
+ `${this.name}: unsupported image role "${role}" for Veo video generation.`
148
+ );
149
+ }
150
+ }
151
+ return { image, lastFrame, referenceImages };
152
+ }
153
+ async getVideoStatus(jobId) {
154
+ const operation = await this.getOperation(jobId);
155
+ if (!operation.done) {
156
+ return { jobId, status: "processing" };
157
+ }
158
+ if (operation.error) {
159
+ return {
160
+ jobId,
161
+ status: "failed",
162
+ error: operationErrorMessage(operation.error)
163
+ };
164
+ }
165
+ const videos = operation.response?.generatedVideos ?? [];
166
+ if (videos.length === 0) {
167
+ const reasons = operation.response?.raiMediaFilteredReasons;
168
+ return {
169
+ jobId,
170
+ status: "failed",
171
+ error: reasons?.length ? `Video was filtered by Responsible-AI: ${reasons.join("; ")}` : "Veo returned no generated videos."
172
+ };
173
+ }
174
+ return { jobId, status: "completed" };
175
+ }
176
+ async getVideoUrl(jobId) {
177
+ const operation = await this.getOperation(jobId);
178
+ if (!operation.done) {
179
+ throw new Error(
180
+ `Video is not ready yet. Check status first. Job ID: ${jobId}`
181
+ );
182
+ }
183
+ if (operation.error) {
184
+ throw new Error(
185
+ `Video generation failed: ${operationErrorMessage(operation.error)}`
186
+ );
187
+ }
188
+ const uri = operation.response?.generatedVideos?.[0]?.video?.uri;
189
+ if (!uri) {
190
+ const reasons = operation.response?.raiMediaFilteredReasons;
191
+ throw new Error(
192
+ reasons?.length ? `Video was filtered by Responsible-AI: ${reasons.join("; ")}` : `Video URL not found in operation response. Job ID: ${jobId}`
193
+ );
194
+ }
195
+ return { jobId, url: uri };
196
+ }
197
+ availableDurations() {
198
+ return getGeminiVideoDurationOptions(this.model);
199
+ }
200
+ snapDuration(seconds) {
201
+ return snapToDurationOption(seconds, this.availableDurations());
202
+ }
203
+ /**
204
+ * Fetch the long-running operation by name. The SDK's
205
+ * `operations.getVideosOperation` needs a real `GenerateVideosOperation`
206
+ * instance (it calls `_fromAPIResponse` on it), so reconstruct one from
207
+ * the job ID rather than passing an object literal.
208
+ */
209
+ async getOperation(jobId) {
210
+ const operation = new GenerateVideosOperation();
211
+ operation.name = jobId;
212
+ return await this.client.operations.getVideosOperation({ operation });
213
+ }
214
+ }
215
+ function createGeminiVideo(model, apiKey, config) {
216
+ return new GeminiVideoAdapter({ apiKey, ...config }, model);
217
+ }
218
+ function geminiVideo(model, config) {
219
+ const apiKey = getGeminiApiKeyFromEnv();
220
+ return createGeminiVideo(model, apiKey, config);
221
+ }
222
+ export {
223
+ GeminiVideoAdapter,
224
+ createGeminiVideo,
225
+ geminiVideo
226
+ };
227
+ //# sourceMappingURL=video.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video.js","sources":["../../../src/adapters/video.ts"],"sourcesContent":["import {\n GenerateVideosOperation,\n VideoGenerationReferenceType,\n} from '@google/genai'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'\nimport { arrayBufferToBase64 } from '@tanstack/ai-utils'\nimport { createGeminiClient, getGeminiApiKeyFromEnv } from '../utils'\nimport { getGeminiVideoDurationOptions } from '../video/video-provider-options'\nimport type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n ImagePart,\n MediaInputMetadata,\n VideoGenerationOptions,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n GenerateVideosConfig,\n GoogleGenAI,\n Image,\n VideoGenerationReferenceImage,\n} from '@google/genai'\nimport type {\n GeminiVideoModel,\n GeminiVideoModelDurationByName,\n GeminiVideoModelInputModalitiesByName,\n GeminiVideoModelProviderOptionsByName,\n GeminiVideoModelSizeByName,\n GeminiVideoProviderOptions,\n GeminiVideoSize,\n} from '../video/video-provider-options'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for Gemini video adapter.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface GeminiVideoConfig extends GeminiClientConfig {}\n\n/**\n * Extract a human-readable message from a long-running operation's error,\n * which the SDK types as `Record<string, unknown>` (a google.rpc.Status).\n */\nfunction operationErrorMessage(error: Record<string, unknown>): string {\n if (typeof error.message === 'string' && error.message.length > 0) {\n return error.message\n }\n return JSON.stringify(error)\n}\n\n/**\n * Convert a TanStack image prompt part into the genai `Image` shape Veo\n * accepts: base64 `imageBytes` (data sources, data: URIs, fetched HTTP\n * URLs) or a `gcsUri` passthrough for Cloud Storage references.\n */\nasync function imagePartToVeoImage(\n part: ImagePart<MediaInputMetadata>,\n): Promise<Image> {\n if (part.source.type === 'data') {\n return {\n imageBytes: part.source.value,\n mimeType: part.source.mimeType || 'image/png',\n }\n }\n const url = part.source.value\n if (url.startsWith('gs://')) {\n return {\n gcsUri: url,\n ...(part.source.mimeType && { mimeType: part.source.mimeType }),\n }\n }\n if (url.startsWith('data:')) {\n const match = url.match(/^data:([^;,]+)?(;base64)?,(.*)$/)\n if (!match || !match[2]) {\n throw new Error(\n 'gemini: only base64 data: URIs are supported for video image inputs.',\n )\n }\n return {\n imageBytes: match[3] ?? '',\n mimeType: match[1] || part.source.mimeType || 'image/png',\n }\n }\n const response = await fetch(url)\n if (!response.ok) {\n throw new Error(\n `Failed to fetch image input (${response.status} ${response.statusText}): ${url}`,\n )\n }\n const blob = await response.blob()\n const buffer = await blob.arrayBuffer()\n return {\n imageBytes: arrayBufferToBase64(buffer),\n mimeType: part.source.mimeType || blob.type || 'image/png',\n }\n}\n\n/**\n * Gemini Veo Video Generation Adapter\n *\n * Tree-shakeable adapter for Google Veo video generation. Veo runs as a\n * long-running operation: `createVideoJob` starts the operation via the\n * `:predictLongRunning` endpoint, `getVideoStatus` polls it, and\n * `getVideoUrl` extracts the generated video's URI once it completes.\n *\n * Image prompt parts are routed by `metadata.role`:\n * - `'start_frame'` (or the first un-roled image) → the input image the\n * video starts from\n * - `'end_frame'` → `lastFrame` (the frame the video ends on)\n * - `'reference'` / `'character'` → `referenceImages` (asset references,\n * Veo 3.1)\n *\n * Note: the returned video URI is served by the Gemini Files API and\n * requires the API key (`x-goog-api-key` header or `?key=` query\n * parameter) to download.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport class GeminiVideoAdapter<\n TModel extends GeminiVideoModel,\n> extends BaseVideoAdapter<\n TModel,\n GeminiVideoProviderOptions,\n GeminiVideoModelProviderOptionsByName,\n GeminiVideoModelSizeByName,\n GeminiVideoModelInputModalitiesByName,\n GeminiVideoModelDurationByName\n> {\n readonly name = 'gemini' as const\n\n protected client: GoogleGenAI\n\n constructor(config: GeminiVideoConfig, model: TModel) {\n super({}, model)\n this.client = createGeminiClient(config)\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n GeminiVideoProviderOptions,\n GeminiVideoSize,\n GeminiVideoModelDurationByName[TModel]\n >,\n ): Promise<VideoJobResult> {\n const { prompt, size, duration, modelOptions, logger } = options\n\n logger.request(\n `activity=video.create provider=${this.name} model=${this.model} size=${size ?? 'default'} duration=${duration ?? 'default'}`,\n { provider: this.name, model: this.model },\n )\n\n try {\n const resolved = resolveMediaPrompt(prompt)\n\n if (resolved.videos.length > 0) {\n throw new Error(\n `${this.name}.createVideoJob does not support video prompt parts (model: ${this.model}).`,\n )\n }\n if (resolved.audios.length > 0) {\n throw new Error(\n `${this.name}.createVideoJob does not support audio prompt parts (model: ${this.model}).`,\n )\n }\n\n const { image, lastFrame, referenceImages } = await this.routeImageParts(\n resolved.images,\n )\n\n const config: GenerateVideosConfig = {\n ...modelOptions,\n ...(size !== undefined && { aspectRatio: size }),\n ...(duration !== undefined && { durationSeconds: duration }),\n ...(lastFrame && { lastFrame }),\n ...(referenceImages.length > 0 && { referenceImages }),\n }\n\n const operation = await this.client.models.generateVideos({\n model: this.model,\n prompt: resolved.text,\n ...(image && { image }),\n config,\n })\n\n if (!operation.name) {\n throw new Error(\n 'Veo did not return an operation name for the video generation job.',\n )\n }\n\n return { jobId: operation.name, model: this.model }\n } catch (error) {\n logger.errors(`${this.name}.createVideoJob fatal`, {\n error,\n source: `${this.name}.createVideoJob`,\n })\n throw error\n }\n }\n\n /**\n * Route image prompt parts onto Veo's request fields by `metadata.role`.\n */\n private async routeImageParts(\n parts: Array<ImagePart<MediaInputMetadata>>,\n ): Promise<{\n image: Image | undefined\n lastFrame: Image | undefined\n referenceImages: Array<VideoGenerationReferenceImage>\n }> {\n let image: Image | undefined\n let lastFrame: Image | undefined\n const referenceImages: Array<VideoGenerationReferenceImage> = []\n\n for (const part of parts) {\n const role = part.metadata?.role\n switch (role) {\n case 'end_frame': {\n if (lastFrame) {\n throw new Error(\n `${this.name}: Veo accepts at most one 'end_frame' image.`,\n )\n }\n lastFrame = await imagePartToVeoImage(part)\n break\n }\n case 'reference':\n case 'character': {\n referenceImages.push({\n image: await imagePartToVeoImage(part),\n referenceType: VideoGenerationReferenceType.ASSET,\n })\n break\n }\n case 'start_frame':\n case undefined: {\n if (image) {\n throw new Error(\n `${this.name}: Veo accepts at most one starting image; received multiple 'start_frame'/un-roled images. Use metadata.role ('end_frame', 'reference') to disambiguate the others.`,\n )\n }\n image = await imagePartToVeoImage(part)\n break\n }\n case 'mask':\n case 'control':\n throw new Error(\n `${this.name}: unsupported image role \"${role}\" for Veo video generation.`,\n )\n }\n }\n\n return { image, lastFrame, referenceImages }\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n const operation = await this.getOperation(jobId)\n\n if (!operation.done) {\n return { jobId, status: 'processing' }\n }\n\n if (operation.error) {\n return {\n jobId,\n status: 'failed',\n error: operationErrorMessage(operation.error),\n }\n }\n\n // The operation can finish \"successfully\" with every sample dropped by\n // Responsible-AI filters — surface that as a failure instead of letting\n // getVideoUrl() throw on an empty response.\n const videos = operation.response?.generatedVideos ?? []\n if (videos.length === 0) {\n const reasons = operation.response?.raiMediaFilteredReasons\n return {\n jobId,\n status: 'failed',\n error: reasons?.length\n ? `Video was filtered by Responsible-AI: ${reasons.join('; ')}`\n : 'Veo returned no generated videos.',\n }\n }\n\n return { jobId, status: 'completed' }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n const operation = await this.getOperation(jobId)\n\n if (!operation.done) {\n throw new Error(\n `Video is not ready yet. Check status first. Job ID: ${jobId}`,\n )\n }\n\n if (operation.error) {\n throw new Error(\n `Video generation failed: ${operationErrorMessage(operation.error)}`,\n )\n }\n\n const uri = operation.response?.generatedVideos?.[0]?.video?.uri\n if (!uri) {\n const reasons = operation.response?.raiMediaFilteredReasons\n throw new Error(\n reasons?.length\n ? `Video was filtered by Responsible-AI: ${reasons.join('; ')}`\n : `Video URL not found in operation response. Job ID: ${jobId}`,\n )\n }\n\n return { jobId, url: uri }\n }\n\n override availableDurations(): DurationOptions<\n GeminiVideoModelDurationByName[TModel]\n > {\n return getGeminiVideoDurationOptions(this.model)\n }\n\n override snapDuration(\n seconds: number,\n ): GeminiVideoModelDurationByName[TModel] | undefined {\n return snapToDurationOption(seconds, this.availableDurations())\n }\n\n /**\n * Fetch the long-running operation by name. The SDK's\n * `operations.getVideosOperation` needs a real `GenerateVideosOperation`\n * instance (it calls `_fromAPIResponse` on it), so reconstruct one from\n * the job ID rather than passing an object literal.\n */\n private async getOperation(jobId: string): Promise<GenerateVideosOperation> {\n const operation = new GenerateVideosOperation()\n operation.name = jobId\n return await this.client.operations.getVideosOperation({ operation })\n }\n}\n\n/**\n * Creates a Gemini video adapter with an explicit API key.\n * Type resolution happens here at the call site.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'veo-3.1-generate-preview')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini video adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiVideo('veo-3.1-generate-preview', 'your-api-key');\n *\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'A beautiful sunset over the ocean',\n * duration: adapter.snapDuration(7), // → 6\n * });\n * ```\n */\nexport function createGeminiVideo<TModel extends GeminiVideoModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiVideoConfig, 'apiKey'>,\n): GeminiVideoAdapter<TModel> {\n return new GeminiVideoAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini video 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 * @experimental Video generation is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'veo-3.1-generate-preview')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini video 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 = geminiVideo('veo-3.1-generate-preview');\n *\n * // Create a video generation job\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'A cat playing piano'\n * });\n *\n * // Poll for status\n * const status = await getVideoJobStatus({ adapter, jobId });\n * ```\n */\nexport function geminiVideo<TModel extends GeminiVideoModel>(\n model: TModel,\n config?: Omit<GeminiVideoConfig, 'apiKey'>,\n): GeminiVideoAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiVideo(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}