@tanstack/ai-gemini 0.32.1 → 0.34.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/README.md +1 -0
- package/dist/esm/adapters/files.d.ts +27 -0
- package/dist/esm/adapters/files.js +62 -0
- package/dist/esm/adapters/files.js.map +1 -0
- package/dist/esm/adapters/image.d.ts +1 -0
- package/dist/esm/adapters/image.js +3 -2
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/text.d.ts +1 -0
- package/dist/esm/adapters/text.js +68 -5
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/adapters/video.d.ts +1 -0
- package/dist/esm/adapters/video.js +8 -5
- package/dist/esm/adapters/video.js.map +1 -1
- package/dist/esm/experimental/text-interactions/adapter.d.ts +1 -0
- package/dist/esm/experimental/text-interactions/adapter.js +11 -9
- package/dist/esm/experimental/text-interactions/adapter.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/message-types.d.ts +6 -1
- package/package.json +4 -3
- package/src/adapters/files.ts +84 -0
- package/src/adapters/image.ts +13 -3
- package/src/adapters/text.ts +105 -8
- package/src/adapters/video.ts +27 -5
- package/src/experimental/text-interactions/adapter.ts +18 -9
- package/src/index.ts +8 -0
- package/src/message-types.ts +7 -1
package/README.md
CHANGED
|
@@ -240,6 +240,7 @@ Official adapters include:
|
|
|
240
240
|
| [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
|
|
241
241
|
| [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
|
|
242
242
|
| [`@tanstack/ai-reactor`](https://tanstack.com/ai/latest/docs/adapters/reactor) | Reactor live world and video generation (Orbis, Happy Oyster, LingBot, Helios, FastH3) |
|
|
243
|
+
| [`@tanstack/ai-worldlabs`](https://tanstack.com/ai/latest/docs/adapters/worldlabs) | World Labs Marble persistent 3D world generation |
|
|
243
244
|
| [`@tanstack/ai-cloudflare`](https://tanstack.com/ai/latest/docs/adapters/cloudflare) | Cloudflare Workers AI chat, embeddings, image, speech, transcription, and AI Gateway |
|
|
244
245
|
|
|
245
246
|
The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BaseFilesAdapter, FileHandle, FileUploadInput } from '@tanstack/ai/adapters';
|
|
2
|
+
import { GeminiClientConfig } from '../utils/client.js';
|
|
3
|
+
export interface GeminiFilesConfig extends GeminiClientConfig {
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Gemini Files adapter — uploads media to the Gemini Files API and references
|
|
7
|
+
* it by its file URI. Pair with `geminiText()` / `geminiImage()`: reference the
|
|
8
|
+
* returned handle via `fileSourceFromHandle(handle)`, which uses the handle URI
|
|
9
|
+
* (Gemini fetches it server-side as `fileData.fileUri`).
|
|
10
|
+
*/
|
|
11
|
+
export declare class GeminiFilesAdapter extends BaseFilesAdapter<'gemini'> {
|
|
12
|
+
readonly name: "gemini";
|
|
13
|
+
private readonly client;
|
|
14
|
+
constructor(config: GeminiFilesConfig);
|
|
15
|
+
upload(input: FileUploadInput): Promise<FileHandle<'gemini'>>;
|
|
16
|
+
get(id: string): Promise<FileHandle<'gemini'>>;
|
|
17
|
+
delete(id: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create a Gemini Files adapter with an explicit API key.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createGeminiFiles(apiKey: string, config?: Omit<GeminiFilesConfig, 'apiKey'>): GeminiFilesAdapter;
|
|
23
|
+
/**
|
|
24
|
+
* Create a Gemini Files adapter, reading the API key from `GOOGLE_API_KEY` /
|
|
25
|
+
* `GEMINI_API_KEY`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function geminiFiles(config?: Omit<GeminiFilesConfig, 'apiKey'>): GeminiFilesAdapter;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createGeminiClient, getGeminiApiKeyFromEnv } from "../utils/client.js";
|
|
2
|
+
import { BaseFilesAdapter, normalizeFileUploadInput } from "@tanstack/ai/adapters";
|
|
3
|
+
//#region src/adapters/files.ts
|
|
4
|
+
/**
|
|
5
|
+
* Gemini Files adapter — uploads media to the Gemini Files API and references
|
|
6
|
+
* it by its file URI. Pair with `geminiText()` / `geminiImage()`: reference the
|
|
7
|
+
* returned handle via `fileSourceFromHandle(handle)`, which uses the handle URI
|
|
8
|
+
* (Gemini fetches it server-side as `fileData.fileUri`).
|
|
9
|
+
*/
|
|
10
|
+
var GeminiFilesAdapter = class extends BaseFilesAdapter {
|
|
11
|
+
name = "gemini";
|
|
12
|
+
client;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
super();
|
|
15
|
+
this.client = createGeminiClient(config);
|
|
16
|
+
}
|
|
17
|
+
async upload(input) {
|
|
18
|
+
const { blob, mimeType } = normalizeFileUploadInput(input);
|
|
19
|
+
return toFileHandle(await this.client.files.upload({
|
|
20
|
+
file: blob,
|
|
21
|
+
...mimeType ? { config: { mimeType } } : {}
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
async get(id) {
|
|
25
|
+
return toFileHandle(await this.client.files.get({ name: id }));
|
|
26
|
+
}
|
|
27
|
+
async delete(id) {
|
|
28
|
+
await this.client.files.delete({ name: id });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function toFileHandle(file) {
|
|
32
|
+
if (!file.name) throw new Error("gemini: files.upload returned a file without a name");
|
|
33
|
+
const expiresAt = file.expirationTime ? Date.parse(file.expirationTime) : void 0;
|
|
34
|
+
return {
|
|
35
|
+
id: file.name,
|
|
36
|
+
provider: "gemini",
|
|
37
|
+
...file.uri ? { uri: file.uri } : {},
|
|
38
|
+
...file.mimeType ? { mimeType: file.mimeType } : {},
|
|
39
|
+
...file.sizeBytes ? { sizeBytes: Number(file.sizeBytes) } : {},
|
|
40
|
+
...expiresAt !== void 0 && !Number.isNaN(expiresAt) ? { expiresAt } : {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a Gemini Files adapter with an explicit API key.
|
|
45
|
+
*/
|
|
46
|
+
function createGeminiFiles(apiKey, config) {
|
|
47
|
+
return new GeminiFilesAdapter({
|
|
48
|
+
...config,
|
|
49
|
+
apiKey
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a Gemini Files adapter, reading the API key from `GOOGLE_API_KEY` /
|
|
54
|
+
* `GEMINI_API_KEY`.
|
|
55
|
+
*/
|
|
56
|
+
function geminiFiles(config) {
|
|
57
|
+
return createGeminiFiles(getGeminiApiKeyFromEnv(), config);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { GeminiFilesAdapter, createGeminiFiles, geminiFiles };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","names":[],"sources":["../../../src/adapters/files.ts"],"sourcesContent":["import {\n BaseFilesAdapter,\n normalizeFileUploadInput,\n} from '@tanstack/ai/adapters'\nimport { createGeminiClient, getGeminiApiKeyFromEnv } from '../utils/client'\nimport type { File as GeminiFile, GoogleGenAI } from '@google/genai'\nimport type { FileHandle, FileUploadInput } from '@tanstack/ai/adapters'\nimport type { GeminiClientConfig } from '../utils/client'\n\nexport interface GeminiFilesConfig extends GeminiClientConfig {}\n\n/**\n * Gemini Files adapter — uploads media to the Gemini Files API and references\n * it by its file URI. Pair with `geminiText()` / `geminiImage()`: reference the\n * returned handle via `fileSourceFromHandle(handle)`, which uses the handle URI\n * (Gemini fetches it server-side as `fileData.fileUri`).\n */\nexport class GeminiFilesAdapter extends BaseFilesAdapter<'gemini'> {\n readonly name = 'gemini' as const\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiFilesConfig) {\n super()\n this.client = createGeminiClient(config)\n }\n\n async upload(input: FileUploadInput): Promise<FileHandle<'gemini'>> {\n const { blob, mimeType } = normalizeFileUploadInput(input)\n const file = await this.client.files.upload({\n file: blob,\n ...(mimeType ? { config: { mimeType } } : {}),\n })\n return toFileHandle(file)\n }\n\n async get(id: string): Promise<FileHandle<'gemini'>> {\n return toFileHandle(await this.client.files.get({ name: id }))\n }\n\n async delete(id: string): Promise<void> {\n await this.client.files.delete({ name: id })\n }\n}\n\nfunction toFileHandle(file: GeminiFile): FileHandle<'gemini'> {\n // `name` (e.g. \"files/abc-123\") is the lifecycle id; `uri` is the URL Gemini\n // fetches when the handle is referenced in a message.\n if (!file.name) {\n throw new Error('gemini: files.upload returned a file without a name')\n }\n const expiresAt = file.expirationTime\n ? Date.parse(file.expirationTime)\n : undefined\n return {\n id: file.name,\n provider: 'gemini',\n ...(file.uri ? { uri: file.uri } : {}),\n ...(file.mimeType ? { mimeType: file.mimeType } : {}),\n ...(file.sizeBytes ? { sizeBytes: Number(file.sizeBytes) } : {}),\n ...(expiresAt !== undefined && !Number.isNaN(expiresAt)\n ? { expiresAt }\n : {}),\n }\n}\n\n/**\n * Create a Gemini Files adapter with an explicit API key.\n */\nexport function createGeminiFiles(\n apiKey: string,\n config?: Omit<GeminiFilesConfig, 'apiKey'>,\n): GeminiFilesAdapter {\n return new GeminiFilesAdapter({ ...config, apiKey })\n}\n\n/**\n * Create a Gemini Files adapter, reading the API key from `GOOGLE_API_KEY` /\n * `GEMINI_API_KEY`.\n */\nexport function geminiFiles(\n config?: Omit<GeminiFilesConfig, 'apiKey'>,\n): GeminiFilesAdapter {\n return createGeminiFiles(getGeminiApiKeyFromEnv(), config)\n}\n"],"mappings":";;;;;;;;;AAiBA,IAAa,qBAAb,cAAwC,iBAA2B;CACjE,OAAgB;CAChB;CAEA,YAAY,QAA2B;EACrC,MAAM;EACN,KAAK,SAAS,mBAAmB,MAAM;CACzC;CAEA,MAAM,OAAO,OAAuD;EAClE,MAAM,EAAE,MAAM,aAAa,yBAAyB,KAAK;EAKzD,OAAO,aAAa,MAJD,KAAK,OAAO,MAAM,OAAO;GAC1C,MAAM;GACN,GAAI,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC;EAC7C,CAAC,CACuB;CAC1B;CAEA,MAAM,IAAI,IAA2C;EACnD,OAAO,aAAa,MAAM,KAAK,OAAO,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;CAC/D;CAEA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,OAAO,MAAM,OAAO,EAAE,MAAM,GAAG,CAAC;CAC7C;AACF;AAEA,SAAS,aAAa,MAAwC;CAG5D,IAAI,CAAC,KAAK,MACR,MAAM,IAAI,MAAM,qDAAqD;CAEvE,MAAM,YAAY,KAAK,iBACnB,KAAK,MAAM,KAAK,cAAc,IAC9B,KAAA;CACJ,OAAO;EACL,IAAI,KAAK;EACT,UAAU;EACV,GAAI,KAAK,MAAM,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;EACpC,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;EACnD,GAAI,KAAK,YAAY,EAAE,WAAW,OAAO,KAAK,SAAS,EAAE,IAAI,CAAC;EAC9D,GAAI,cAAc,KAAA,KAAa,CAAC,OAAO,MAAM,SAAS,IAClD,EAAE,UAAU,IACZ,CAAC;CACP;AACF;;;;AAKA,SAAgB,kBACd,QACA,QACoB;CACpB,OAAO,IAAI,mBAAmB;EAAE,GAAG;EAAQ;CAAO,CAAC;AACrD;;;;;AAMA,SAAgB,YACd,QACoB;CACpB,OAAO,kBAAkB,uBAAuB,GAAG,MAAM;AAC3D"}
|
|
@@ -27,6 +27,7 @@ export type GeminiImageModel = GeminiImageModels;
|
|
|
27
27
|
export declare class GeminiImageAdapter<TModel extends GeminiImageModel> extends BaseImageAdapter<TModel, GeminiAnyImageProviderOptions, GeminiImageModelProviderOptionsByName, GeminiImageModelSizeByName, GeminiImageModelInputModalitiesByName> {
|
|
28
28
|
readonly kind: "image";
|
|
29
29
|
readonly name: "gemini";
|
|
30
|
+
readonly supportsFileSources = true;
|
|
30
31
|
'~types': {
|
|
31
32
|
providerOptions: GeminiAnyImageProviderOptions;
|
|
32
33
|
modelProviderOptionsByName: GeminiImageModelProviderOptionsByName;
|
|
@@ -2,7 +2,7 @@ import { buildGeminiUsage } from "../usage.js";
|
|
|
2
2
|
import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils/client.js";
|
|
3
3
|
import "../utils/index.js";
|
|
4
4
|
import { isGeminiNativeImageModel, parseNativeImageSize, sizeToAspectRatio, validateImageSize, validateNumberOfImages, validatePrompt } from "../image/image-provider-options.js";
|
|
5
|
-
import { resolveMediaPrompt } from "@tanstack/ai";
|
|
5
|
+
import { fileReferenceFor, isFileSource, resolveMediaPrompt } from "@tanstack/ai";
|
|
6
6
|
import { BaseImageAdapter } from "@tanstack/ai/adapters";
|
|
7
7
|
//#region src/adapters/image.ts
|
|
8
8
|
/**
|
|
@@ -22,6 +22,7 @@ import { BaseImageAdapter } from "@tanstack/ai/adapters";
|
|
|
22
22
|
var GeminiImageAdapter = class extends BaseImageAdapter {
|
|
23
23
|
kind = "image";
|
|
24
24
|
name = "gemini";
|
|
25
|
+
supportsFileSources = true;
|
|
25
26
|
client;
|
|
26
27
|
constructor(config, model) {
|
|
27
28
|
super(model, config);
|
|
@@ -114,7 +115,7 @@ var GeminiImageAdapter = class extends BaseImageAdapter {
|
|
|
114
115
|
data: part.source.value
|
|
115
116
|
} };
|
|
116
117
|
return { fileData: {
|
|
117
|
-
fileUri: part.source.value,
|
|
118
|
+
fileUri: isFileSource(part.source) ? fileReferenceFor(part.source, this.name) : part.source.value,
|
|
118
119
|
mimeType: part.source.mimeType ?? "image/jpeg"
|
|
119
120
|
} };
|
|
120
121
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { buildGeminiUsage } from '../usage'\nimport {\n isGeminiNativeImageModel,\n parseNativeImageSize,\n sizeToAspectRatio,\n validateImageSize,\n validateNumberOfImages,\n validatePrompt,\n} from '../image/image-provider-options'\nimport type { GeminiImageModels } from '../model-meta'\nimport type {\n GeminiAnyImageProviderOptions,\n GeminiImageModelInputModalitiesByName,\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName,\n GeminiNativeImageProviderOptions,\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 ImageConfig,\n Part,\n} from '@google/genai'\nimport type { GeminiClientConfig } from '../utils/client'\n\n/**\n * Configuration for Gemini image adapter\n */\nexport interface GeminiImageConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini Image */\nexport type GeminiImageModel = GeminiImageModels\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 GeminiAnyImageProviderOptions,\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: GeminiAnyImageProviderOptions\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<GeminiAnyImageProviderOptions>,\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 (isGeminiNativeImageModel(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 async generateWithGeminiApi(\n options: ImageGenerationOptions<GeminiNativeImageProviderOptions>,\n resolved: ResolvedMediaPrompt,\n ): Promise<ImageGenerationResult> {\n const { model, size, numberOfImages, modelOptions } = options\n\n const parsedSize = size ? parseNativeImageSize(size) : undefined\n\n // The portable `size` option is the baseline; modelOptions.imageConfig is\n // the provider escape hatch and wins per field, so a caller passing only\n // `imageConfig.imageSize` keeps the aspectRatio derived from `size`.\n const imageConfig: ImageConfig = {\n ...(parsedSize?.aspectRatio && { aspectRatio: parsedSize.aspectRatio }),\n ...(parsedSize?.resolution && { imageSize: parsedSize.resolution }),\n ...modelOptions?.imageConfig,\n }\n\n // Named picks, never a wholesale spread: the Imagen-shaped fields of\n // GeminiImageProviderOptions (personGeneration, safetyFilterLevel,\n // addWatermark, outputMimeType, …) are only valid on GenerateImagesConfig\n // and would be rejected by generateContent. Picking by name means no\n // Imagen field can reach this path even if one slips past the per-model\n // provider-options map.\n const nativeConfig: GenerateContentConfig = {\n ...(modelOptions?.seed !== undefined && { seed: modelOptions.seed }),\n ...(modelOptions?.safetySettings !== undefined && {\n safetySettings: modelOptions.safetySettings,\n }),\n ...(modelOptions?.thinkingConfig !== undefined && {\n thinkingConfig: modelOptions.thinkingConfig,\n }),\n ...(modelOptions?.systemInstruction !== undefined && {\n systemInstruction: modelOptions.systemInstruction,\n }),\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 ...(Object.keys(imageConfig).length > 0 && { imageConfig }),\n }\n\n const contents = 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 buildContents(\n resolved: ResolvedMediaPrompt,\n numberOfImages: number | undefined,\n ): 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> = resolved.parts.map((part) => {\n if (part.type === 'text') {\n return { 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 if (countInstruction) {\n parts.push({ text: countInstruction })\n }\n return [{ role: 'user', parts }]\n }\n\n private imagePartToGeminiPart(part: ImagePart<MediaInputMetadata>): 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 // URL sources (public HTTPS, Files API URIs, gs://) pass through as\n // `fileData` and Gemini fetches them server-side — same as the chat\n // adapter. Fetching locally and inlining as base64 double-buffers the\n // image and OOMs on memory-constrained runtimes (e.g. Cloudflare\n // Workers).\n return {\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? 'image/jpeg',\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<GeminiAnyImageProviderOptions>,\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\n // Named picks, never a wholesale spread — the mirror image of the native\n // path below. A native-only field (safetySettings, thinkingConfig,\n // imageConfig, systemInstruction) belongs to GenerateContentConfig and is\n // rejected by generateImages with 400 INVALID_ARGUMENT, so it must not be\n // able to reach here even when the caller's `modelOptions` was typed\n // against both shapes at once (e.g. an adapter inferred from a union of\n // model names).\n return {\n numberOfImages: numberOfImages ?? 1,\n // Map size to aspect ratio if provided; modelOptions.aspectRatio,\n // picked after it, overrides.\n ...(sizeAspectRatio !== undefined && { aspectRatio: sizeAspectRatio }),\n ...(modelOptions?.aspectRatio !== undefined && {\n aspectRatio: modelOptions.aspectRatio,\n }),\n ...(modelOptions?.personGeneration !== undefined && {\n personGeneration: modelOptions.personGeneration,\n }),\n ...(modelOptions?.safetyFilterLevel !== undefined && {\n safetyFilterLevel: modelOptions.safetyFilterLevel,\n }),\n ...(modelOptions?.seed !== undefined && { seed: modelOptions.seed }),\n ...(modelOptions?.addWatermark !== undefined && {\n addWatermark: modelOptions.addWatermark,\n }),\n ...(modelOptions?.language !== undefined && {\n language: modelOptions.language,\n }),\n ...(modelOptions?.negativePrompt !== undefined && {\n negativePrompt: modelOptions.negativePrompt,\n }),\n ...(modelOptions?.outputMimeType !== undefined && {\n outputMimeType: modelOptions.outputMimeType,\n }),\n ...(modelOptions?.outputCompressionQuality !== undefined && {\n outputCompressionQuality: modelOptions.outputCompressionQuality,\n }),\n ...(modelOptions?.guidanceScale !== undefined && {\n guidanceScale: modelOptions.guidanceScale,\n }),\n ...(modelOptions?.enhancePrompt !== undefined && {\n enhancePrompt: modelOptions.enhancePrompt,\n }),\n ...(modelOptions?.includeSafetyAttributes !== undefined && {\n includeSafetyAttributes: modelOptions.includeSafetyAttributes,\n }),\n ...(modelOptions?.includeRaiReason !== undefined && {\n includeRaiReason: modelOptions.includeRaiReason,\n }),\n ...(modelOptions?.outputGcsUri !== undefined && {\n outputGcsUri: modelOptions.outputGcsUri,\n }),\n ...(modelOptions?.labels !== undefined && {\n labels: modelOptions.labels,\n }),\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/** @deprecated Shut down 2026-06-25. Use `gemini-3.1-flash-image`. */\nexport function createGeminiImage(\n model: 'gemini-3.1-flash-image-preview',\n apiKey: string,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<'gemini-3.1-flash-image-preview'>\n/** @deprecated Shut down 2026-06-25. Use `gemini-3-pro-image`. */\nexport function createGeminiImage(\n model: 'gemini-3-pro-image-preview',\n apiKey: string,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<'gemini-3-pro-image-preview'>\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-4.0-generate-001')\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-4.0-generate-001', \"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>\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/** @deprecated Shut down 2026-06-25. Use `gemini-3.1-flash-image`. */\nexport function geminiImage(\n model: 'gemini-3.1-flash-image-preview',\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<'gemini-3.1-flash-image-preview'>\n/** @deprecated Shut down 2026-06-25. Use `gemini-3-pro-image`. */\nexport function geminiImage(\n model: 'gemini-3-pro-image-preview',\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<'gemini-3-pro-image-preview'>\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>\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import {\n fileReferenceFor,\n isFileSource,\n resolveMediaPrompt,\n} from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { buildGeminiUsage } from '../usage'\nimport {\n isGeminiNativeImageModel,\n parseNativeImageSize,\n sizeToAspectRatio,\n validateImageSize,\n validateNumberOfImages,\n validatePrompt,\n} from '../image/image-provider-options'\nimport type { GeminiImageModels } from '../model-meta'\nimport type {\n GeminiAnyImageProviderOptions,\n GeminiImageModelInputModalitiesByName,\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName,\n GeminiNativeImageProviderOptions,\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 ImageConfig,\n Part,\n} from '@google/genai'\nimport type { GeminiClientConfig } from '../utils/client'\n\n/**\n * Configuration for Gemini image adapter\n */\nexport interface GeminiImageConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini Image */\nexport type GeminiImageModel = GeminiImageModels\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 GeminiAnyImageProviderOptions,\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName,\n GeminiImageModelInputModalitiesByName\n> {\n override readonly kind = 'image' as const\n readonly name = 'gemini' as const\n // Consumes Gemini Files API references (geminiFiles()) as fileData.fileUri.\n override readonly supportsFileSources = true\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: GeminiAnyImageProviderOptions\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<GeminiAnyImageProviderOptions>,\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 (isGeminiNativeImageModel(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 async generateWithGeminiApi(\n options: ImageGenerationOptions<GeminiNativeImageProviderOptions>,\n resolved: ResolvedMediaPrompt,\n ): Promise<ImageGenerationResult> {\n const { model, size, numberOfImages, modelOptions } = options\n\n const parsedSize = size ? parseNativeImageSize(size) : undefined\n\n // The portable `size` option is the baseline; modelOptions.imageConfig is\n // the provider escape hatch and wins per field, so a caller passing only\n // `imageConfig.imageSize` keeps the aspectRatio derived from `size`.\n const imageConfig: ImageConfig = {\n ...(parsedSize?.aspectRatio && { aspectRatio: parsedSize.aspectRatio }),\n ...(parsedSize?.resolution && { imageSize: parsedSize.resolution }),\n ...modelOptions?.imageConfig,\n }\n\n // Named picks, never a wholesale spread: the Imagen-shaped fields of\n // GeminiImageProviderOptions (personGeneration, safetyFilterLevel,\n // addWatermark, outputMimeType, …) are only valid on GenerateImagesConfig\n // and would be rejected by generateContent. Picking by name means no\n // Imagen field can reach this path even if one slips past the per-model\n // provider-options map.\n const nativeConfig: GenerateContentConfig = {\n ...(modelOptions?.seed !== undefined && { seed: modelOptions.seed }),\n ...(modelOptions?.safetySettings !== undefined && {\n safetySettings: modelOptions.safetySettings,\n }),\n ...(modelOptions?.thinkingConfig !== undefined && {\n thinkingConfig: modelOptions.thinkingConfig,\n }),\n ...(modelOptions?.systemInstruction !== undefined && {\n systemInstruction: modelOptions.systemInstruction,\n }),\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 ...(Object.keys(imageConfig).length > 0 && { imageConfig }),\n }\n\n const contents = 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 buildContents(\n resolved: ResolvedMediaPrompt,\n numberOfImages: number | undefined,\n ): 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> = resolved.parts.map((part) => {\n if (part.type === 'text') {\n return { 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 if (countInstruction) {\n parts.push({ text: countInstruction })\n }\n return [{ role: 'user', parts }]\n }\n\n private imagePartToGeminiPart(part: ImagePart<MediaInputMetadata>): 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 // URL sources (public HTTPS, Files API URIs, gs://) pass through as\n // `fileData` and Gemini fetches them server-side — same as the chat\n // adapter. Fetching locally and inlining as base64 double-buffers the\n // image and OOMs on memory-constrained runtimes (e.g. Cloudflare\n // Workers). A file source's handle is the file URI (throws when another\n // provider issued it).\n const fileUri = isFileSource(part.source)\n ? fileReferenceFor(part.source, this.name)\n : part.source.value\n return {\n fileData: {\n fileUri,\n mimeType: part.source.mimeType ?? 'image/jpeg',\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<GeminiAnyImageProviderOptions>,\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\n // Named picks, never a wholesale spread — the mirror image of the native\n // path below. A native-only field (safetySettings, thinkingConfig,\n // imageConfig, systemInstruction) belongs to GenerateContentConfig and is\n // rejected by generateImages with 400 INVALID_ARGUMENT, so it must not be\n // able to reach here even when the caller's `modelOptions` was typed\n // against both shapes at once (e.g. an adapter inferred from a union of\n // model names).\n return {\n numberOfImages: numberOfImages ?? 1,\n // Map size to aspect ratio if provided; modelOptions.aspectRatio,\n // picked after it, overrides.\n ...(sizeAspectRatio !== undefined && { aspectRatio: sizeAspectRatio }),\n ...(modelOptions?.aspectRatio !== undefined && {\n aspectRatio: modelOptions.aspectRatio,\n }),\n ...(modelOptions?.personGeneration !== undefined && {\n personGeneration: modelOptions.personGeneration,\n }),\n ...(modelOptions?.safetyFilterLevel !== undefined && {\n safetyFilterLevel: modelOptions.safetyFilterLevel,\n }),\n ...(modelOptions?.seed !== undefined && { seed: modelOptions.seed }),\n ...(modelOptions?.addWatermark !== undefined && {\n addWatermark: modelOptions.addWatermark,\n }),\n ...(modelOptions?.language !== undefined && {\n language: modelOptions.language,\n }),\n ...(modelOptions?.negativePrompt !== undefined && {\n negativePrompt: modelOptions.negativePrompt,\n }),\n ...(modelOptions?.outputMimeType !== undefined && {\n outputMimeType: modelOptions.outputMimeType,\n }),\n ...(modelOptions?.outputCompressionQuality !== undefined && {\n outputCompressionQuality: modelOptions.outputCompressionQuality,\n }),\n ...(modelOptions?.guidanceScale !== undefined && {\n guidanceScale: modelOptions.guidanceScale,\n }),\n ...(modelOptions?.enhancePrompt !== undefined && {\n enhancePrompt: modelOptions.enhancePrompt,\n }),\n ...(modelOptions?.includeSafetyAttributes !== undefined && {\n includeSafetyAttributes: modelOptions.includeSafetyAttributes,\n }),\n ...(modelOptions?.includeRaiReason !== undefined && {\n includeRaiReason: modelOptions.includeRaiReason,\n }),\n ...(modelOptions?.outputGcsUri !== undefined && {\n outputGcsUri: modelOptions.outputGcsUri,\n }),\n ...(modelOptions?.labels !== undefined && {\n labels: modelOptions.labels,\n }),\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/** @deprecated Shut down 2026-06-25. Use `gemini-3.1-flash-image`. */\nexport function createGeminiImage(\n model: 'gemini-3.1-flash-image-preview',\n apiKey: string,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<'gemini-3.1-flash-image-preview'>\n/** @deprecated Shut down 2026-06-25. Use `gemini-3-pro-image`. */\nexport function createGeminiImage(\n model: 'gemini-3-pro-image-preview',\n apiKey: string,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<'gemini-3-pro-image-preview'>\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-4.0-generate-001')\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-4.0-generate-001', \"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>\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/** @deprecated Shut down 2026-06-25. Use `gemini-3.1-flash-image`. */\nexport function geminiImage(\n model: 'gemini-3.1-flash-image-preview',\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<'gemini-3.1-flash-image-preview'>\n/** @deprecated Shut down 2026-06-25. Use `gemini-3-pro-image`. */\nexport function geminiImage(\n model: 'gemini-3-pro-image-preview',\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<'gemini-3-pro-image-preview'>\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>\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"],"mappings":"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"}
|
|
@@ -37,6 +37,7 @@ type ResolveToolCapabilities<TModel extends string> = TModel extends keyof Gemin
|
|
|
37
37
|
export declare class GeminiTextAdapter<TModel extends (typeof GEMINI_MODELS)[number], TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>, TInputModalities extends ReadonlyArray<Modality> = ResolveInputModalities<TModel>, TToolCapabilities extends ReadonlyArray<string> = ResolveToolCapabilities<TModel>> extends BaseTextAdapter<TModel, TProviderOptions, TInputModalities, GeminiMessageMetadataByModality, TToolCapabilities, GeminiToolCallMetadata> {
|
|
38
38
|
readonly kind: "text";
|
|
39
39
|
readonly name: "gemini";
|
|
40
|
+
readonly supportsFileSources = true;
|
|
40
41
|
private readonly client;
|
|
41
42
|
constructor(config: GeminiTextConfig, model: TModel);
|
|
42
43
|
chatStream(options: TextOptions<GeminiTextProviderOptions>): AsyncIterable<AdapterYieldChunk>;
|
|
@@ -4,7 +4,7 @@ import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils
|
|
|
4
4
|
import "../utils/index.js";
|
|
5
5
|
import { GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS } from "../model-meta.js";
|
|
6
6
|
import { FinishReason } from "@google/genai";
|
|
7
|
-
import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
|
|
7
|
+
import { EventType, fileReferenceFor, isFileSource, normalizeSystemPrompts } from "@tanstack/ai";
|
|
8
8
|
import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
|
|
9
9
|
import { BaseTextAdapter } from "@tanstack/ai/adapters";
|
|
10
10
|
//#region src/adapters/text.ts
|
|
@@ -17,6 +17,23 @@ var DEFAULT_MEDIA_MIME_TYPES = {
|
|
|
17
17
|
video: "video/mp4",
|
|
18
18
|
document: "application/pdf"
|
|
19
19
|
};
|
|
20
|
+
function getGroundingSources(metadata) {
|
|
21
|
+
const sources = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const chunk of metadata.groundingChunks ?? []) {
|
|
23
|
+
const url = chunk.web?.uri;
|
|
24
|
+
if (!url) continue;
|
|
25
|
+
const existing = sources.get(url);
|
|
26
|
+
if (existing) {
|
|
27
|
+
if (!existing.title && chunk.web?.title) existing.title = chunk.web.title;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
sources.set(url, {
|
|
31
|
+
url,
|
|
32
|
+
...chunk.web?.title ? { title: chunk.web.title } : {}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return [...sources.values()];
|
|
36
|
+
}
|
|
20
37
|
/** True when any message carries a video part requesting agentic processing. */
|
|
21
38
|
function hasAgenticVideo(messages) {
|
|
22
39
|
return messages.some((msg) => Array.isArray(msg.content) && msg.content.some((part) => part.type === "video" && part.metadata?.processing === "agentic"));
|
|
@@ -29,7 +46,10 @@ function contentPartToInteraction(part) {
|
|
|
29
46
|
};
|
|
30
47
|
const source = part.source;
|
|
31
48
|
const mimeType = source.type === "data" ? source.mimeType : source.mimeType ?? DEFAULT_MEDIA_MIME_TYPES[part.type];
|
|
32
|
-
const base = source
|
|
49
|
+
const base = isFileSource(source) ? {
|
|
50
|
+
uri: fileReferenceFor(source, "gemini"),
|
|
51
|
+
mime_type: mimeType
|
|
52
|
+
} : source.type === "data" ? {
|
|
33
53
|
data: source.value,
|
|
34
54
|
mime_type: mimeType
|
|
35
55
|
} : {
|
|
@@ -82,6 +102,7 @@ function buildInteractionsInput(messages) {
|
|
|
82
102
|
var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
83
103
|
kind = "text";
|
|
84
104
|
name = "gemini";
|
|
105
|
+
supportsFileSources = true;
|
|
85
106
|
client;
|
|
86
107
|
constructor(config, model) {
|
|
87
108
|
super({}, model);
|
|
@@ -343,6 +364,41 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
343
364
|
let hasEmittedRunStarted = false;
|
|
344
365
|
let hasEmittedTextMessageStart = false;
|
|
345
366
|
let hasEmittedStepStarted = false;
|
|
367
|
+
let groundingMetadata;
|
|
368
|
+
let groundingCallEmitted = false;
|
|
369
|
+
const adapterName = this.name;
|
|
370
|
+
const emitGroundingToolCall = function* () {
|
|
371
|
+
if (!groundingMetadata || groundingCallEmitted) return;
|
|
372
|
+
const sources = getGroundingSources(groundingMetadata);
|
|
373
|
+
if (!(sources.length > 0 || (groundingMetadata.webSearchQueries?.length ?? 0) > 0 || groundingMetadata.searchEntryPoint !== void 0)) return;
|
|
374
|
+
groundingCallEmitted = true;
|
|
375
|
+
const toolCallId = generateId(adapterName);
|
|
376
|
+
const metadata = {
|
|
377
|
+
providerExecuted: true,
|
|
378
|
+
sources,
|
|
379
|
+
gemini: { groundingMetadata }
|
|
380
|
+
};
|
|
381
|
+
yield {
|
|
382
|
+
type: EventType.TOOL_CALL_START,
|
|
383
|
+
toolCallId,
|
|
384
|
+
toolCallName: "google_search",
|
|
385
|
+
toolName: "google_search",
|
|
386
|
+
parentMessageId: messageId,
|
|
387
|
+
model,
|
|
388
|
+
timestamp: Date.now(),
|
|
389
|
+
index: toolCallMap.size,
|
|
390
|
+
metadata
|
|
391
|
+
};
|
|
392
|
+
yield {
|
|
393
|
+
type: EventType.TOOL_CALL_END,
|
|
394
|
+
toolCallId,
|
|
395
|
+
toolCallName: "google_search",
|
|
396
|
+
toolName: "google_search",
|
|
397
|
+
model,
|
|
398
|
+
timestamp: Date.now(),
|
|
399
|
+
input: { ...groundingMetadata.webSearchQueries && { queries: groundingMetadata.webSearchQueries } }
|
|
400
|
+
};
|
|
401
|
+
};
|
|
346
402
|
for await (const chunk of result) {
|
|
347
403
|
logger.provider(`provider=gemini`, { chunk });
|
|
348
404
|
if (!hasEmittedRunStarted) {
|
|
@@ -356,6 +412,7 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
356
412
|
parentRunId: options.parentRunId
|
|
357
413
|
};
|
|
358
414
|
}
|
|
415
|
+
if (chunk.candidates?.[0]?.groundingMetadata) groundingMetadata = chunk.candidates[0].groundingMetadata;
|
|
359
416
|
if (chunk.candidates?.[0]?.content?.parts) {
|
|
360
417
|
const parts = chunk.candidates[0].content.parts;
|
|
361
418
|
for (const part of parts) {
|
|
@@ -517,6 +574,7 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
517
574
|
};
|
|
518
575
|
}
|
|
519
576
|
if (chunk.candidates?.[0]?.finishReason) {
|
|
577
|
+
yield* emitGroundingToolCall();
|
|
520
578
|
const finishReason = chunk.candidates[0].finishReason;
|
|
521
579
|
for (const [toolCallId, toolCallData] of toolCallMap.entries()) {
|
|
522
580
|
let parsedInput = {};
|
|
@@ -581,6 +639,7 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
581
639
|
};
|
|
582
640
|
}
|
|
583
641
|
}
|
|
642
|
+
yield* emitGroundingToolCall();
|
|
584
643
|
}
|
|
585
644
|
convertContentPartToGemini(part) {
|
|
586
645
|
switch (part.type) {
|
|
@@ -589,11 +648,12 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
589
648
|
case "audio":
|
|
590
649
|
case "video":
|
|
591
650
|
case "document": {
|
|
651
|
+
const fileUri = isFileSource(part.source) ? fileReferenceFor(part.source, this.name) : part.source.value;
|
|
592
652
|
const geminiPart = part.source.type === "data" ? { inlineData: {
|
|
593
653
|
data: part.source.value,
|
|
594
654
|
mimeType: part.source.mimeType
|
|
595
655
|
} } : { fileData: {
|
|
596
|
-
fileUri
|
|
656
|
+
fileUri,
|
|
597
657
|
mimeType: part.source.mimeType ?? DEFAULT_MEDIA_MIME_TYPES[part.type]
|
|
598
658
|
} };
|
|
599
659
|
if (part.type === "video") {
|
|
@@ -619,13 +679,15 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
619
679
|
if (Array.isArray(msg.content)) for (const contentPart of msg.content) parts.push(this.convertContentPartToGemini(contentPart));
|
|
620
680
|
else if (msg.content && msg.role !== "tool") parts.push({ text: msg.content });
|
|
621
681
|
if (msg.role === "assistant" && msg.toolCalls?.length) for (const toolCall of msg.toolCalls) {
|
|
682
|
+
const metadata = toolCall.metadata;
|
|
683
|
+
if (metadata?.providerExecuted) continue;
|
|
622
684
|
let parsedArgs = {};
|
|
623
685
|
try {
|
|
624
686
|
parsedArgs = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {};
|
|
625
687
|
} catch {
|
|
626
688
|
parsedArgs = {};
|
|
627
689
|
}
|
|
628
|
-
const thoughtSignature =
|
|
690
|
+
const thoughtSignature = metadata?.thoughtSignature;
|
|
629
691
|
const part = { functionCall: {
|
|
630
692
|
id: toolCall.id,
|
|
631
693
|
name: toolCall.function.name,
|
|
@@ -646,6 +708,7 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
646
708
|
mimeType: part.source.mimeType
|
|
647
709
|
} });
|
|
648
710
|
else {
|
|
711
|
+
const fileUri = isFileSource(part.source) ? fileReferenceFor(part.source, this.name) : part.source.value;
|
|
649
712
|
const defaultMimeType = {
|
|
650
713
|
image: "image/jpeg",
|
|
651
714
|
audio: "audio/mp3",
|
|
@@ -653,7 +716,7 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
653
716
|
document: "application/pdf"
|
|
654
717
|
}[part.type];
|
|
655
718
|
mediaParts.push({ fileData: {
|
|
656
|
-
fileUri
|
|
719
|
+
fileUri,
|
|
657
720
|
mimeType: part.source.mimeType ?? defaultMimeType
|
|
658
721
|
} });
|
|
659
722
|
}
|