diff --git a/.changeset/five-parts-leave.md b/.changeset/five-parts-leave.md new file mode 100644 index 000000000..081995efe --- /dev/null +++ b/.changeset/five-parts-leave.md @@ -0,0 +1,7 @@ +--- +'@tanstack/ai-gemini': minor +--- + +- Add NanoBanana native image generation with up to 4K image output, routing all gemini-\* native image models through generateContent API +- Fix SDK property names (imageGenerationConfig → imageConfig, outputImageSize → imageSize) and rename NanoBanana types to GeminiNativeImage +- Add Gemini 3.1 Pro model support for text generation diff --git a/docs/adapters/fal.md b/docs/adapters/fal.md index 557b07073..5be52698e 100644 --- a/docs/adapters/fal.md +++ b/docs/adapters/fal.md @@ -5,7 +5,7 @@ id: fal-adapter The fal.ai adapter provides access to 600+ models on the fal.ai platform for image generation and video generation. Unlike text-focused adapters, the fal adapter is **media-focused** — it supports `generateImage()` and `generateVideo()` but does not support `chat()` or tools. Audio and speech support are coming soon. -For a full working example, see the [fal.ai example app](https://github.com/TanStack/ai/tree/main/examples/ts-react-fal). +For a full working example, see the [fal.ai example app](https://github.com/TanStack/ai/tree/main/examples/ts-react-media). ## Installation @@ -79,7 +79,7 @@ const proxiedAdapter = falImage("fal-ai/flux/dev", { ## Example: Image Generation -From the [fal.ai example app](https://github.com/TanStack/ai/tree/main/examples/ts-react-fal): +From the [fal.ai example app](https://github.com/TanStack/ai/tree/main/examples/ts-react-media): ```typescript import { generateImage } from "@tanstack/ai"; @@ -155,7 +155,7 @@ import { falVideo } from "@tanstack/ai-fal"; ## Example: Text-to-Video -From the [fal.ai example app](https://github.com/TanStack/ai/tree/main/examples/ts-react-fal): +From the [fal.ai example app](https://github.com/TanStack/ai/tree/main/examples/ts-react-media): ```typescript import { generateVideo, getVideoJobStatus } from "@tanstack/ai"; diff --git a/docs/adapters/gemini.md b/docs/adapters/gemini.md index 53cb479d0..9617c7422 100644 --- a/docs/adapters/gemini.md +++ b/docs/adapters/gemini.md @@ -4,7 +4,9 @@ id: gemini-adapter order: 3 --- -The Google Gemini adapter provides access to Google's Gemini models, including text generation, image generation with Imagen, and experimental text-to-speech. +The Google Gemini adapter provides access to Google's Gemini models, including text generation, image generation with both Imagen and Gemini native image models (NanoBanana), and experimental text-to-speech. + +For a full working example with image generation, see the [media generation example app](https://github.com/TanStack/ai/tree/main/examples/ts-react-media). ## Installation @@ -158,14 +160,39 @@ console.log(result.summary); ## Image Generation -Generate images with Imagen: +The Gemini adapter supports two types of image generation: + +- **Gemini native image models** (NanoBanana) — Use the `generateContent` API with models like `gemini-3.1-flash-image-preview`. These support extended resolution tiers (1K, 2K, 4K) and aspect ratio control. +- **Imagen models** — Use the `generateImages` API with models like `imagen-4.0-generate-001`. These are dedicated image generation models with WIDTHxHEIGHT sizing. + +The adapter automatically routes to the correct API based on the model name — models starting with `gemini-` use `generateContent`, while `imagen-` models use `generateImages`. + +### Example: Gemini Native Image Generation (NanoBanana) + +From the [media generation example app](https://github.com/TanStack/ai/tree/main/examples/ts-react-media): + +```typescript +import { generateImage } from "@tanstack/ai"; +import { geminiImage } from "@tanstack/ai-gemini"; + +const result = await generateImage({ + adapter: geminiImage("gemini-3.1-flash-image-preview"), + prompt: "A futuristic cityscape at sunset", + numberOfImages: 1, + size: "16:9_4K", +}); + +console.log(result.images); +``` + +### Example: Imagen ```typescript import { generateImage } from "@tanstack/ai"; import { geminiImage } from "@tanstack/ai-gemini"; const result = await generateImage({ - adapter: geminiImage("imagen-3.0-generate-002"), + adapter: geminiImage("imagen-4.0-generate-001"), prompt: "A futuristic cityscape at sunset", numberOfImages: 1, }); @@ -173,11 +200,51 @@ const result = await generateImage({ console.log(result.images); ``` +### Image Size Options + +#### Gemini Native Models (NanoBanana) + +Gemini native image models use a template literal size format combining aspect ratio and resolution tier: + +```typescript +// Format: "aspectRatio_resolution" +size: "16:9_4K" +size: "1:1_2K" +size: "9:16_1K" +``` + +| Component | Values | +|-----------|--------| +| Aspect Ratio | `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `9:16`, `16:9`, `21:9` | +| Resolution | `1K`, `2K`, `4K` | + +#### Imagen Models + +Imagen models use WIDTHxHEIGHT format, which maps to aspect ratios internally: + +| Size | Aspect Ratio | +|------|-------------| +| `1024x1024` | 1:1 | +| `1920x1080` | 16:9 | +| `1080x1920` | 9:16 | + +Alternatively, you can specify the aspect ratio directly in Model Options: + +```typescript +const result = await generateImage({ + adapter: geminiImage("imagen-4.0-generate-001"), + prompt: "A landscape photo", + modelOptions: { + aspectRatio: "16:9", + }, +}); +``` + ### Image Model Options ```typescript const result = await generateImage({ - adapter: geminiImage("imagen-3.0-generate-002"), + adapter: geminiImage("imagen-4.0-generate-001"), prompt: "...", modelOptions: { aspectRatio: "16:9", // "1:1" | "3:4" | "4:3" | "9:16" | "16:9" @@ -221,6 +288,30 @@ GOOGLE_API_KEY=your-api-key-here 2. Create a new API key 3. Add it to your environment variables +## Popular Image Models + +### Gemini Native Image Models (NanoBanana) + +These models use the `generateContent` API and support resolution tiers (1K, 2K, 4K). + +| Model | Description | +|-------|-------------| +| `gemini-3.1-flash-image-preview` | Latest and fastest Gemini native image generation | +| `gemini-3-pro-image-preview` | Higher quality Gemini native image generation | +| `gemini-2.5-flash-image` | Gemini 2.5 Flash with image generation | +| `gemini-2.0-flash-preview-image-generation` | Gemini 2.0 Flash image generation | + +### Imagen Models + +These models use the dedicated `generateImages` API. + +| Model | Description | +|-------|-------------| +| `imagen-4.0-ultra-generate-001` | Best quality Imagen image generation | +| `imagen-4.0-generate-001` | High quality Imagen image generation | +| `imagen-4.0-fast-generate-001` | Fast Imagen image generation | +| `imagen-3.0-generate-002` | Imagen 3 image generation | + ## API Reference ### `geminiText(config?)` @@ -252,15 +343,26 @@ Creates a Gemini summarization adapter with an explicit API key. **Returns:** A Gemini summarize adapter instance. -### `geminiImage(config?)` +### `geminiImage(model, config?)` + +Creates a Gemini image adapter using environment variables. Automatically routes to the correct API based on model name — `gemini-*` models use `generateContent`, `imagen-*` models use `generateImages`. + +**Parameters:** -Creates a Gemini image generation adapter using environment variables. +- `model` - The model name (e.g., `"gemini-3.1-flash-image-preview"` or `"imagen-4.0-generate-001"`) +- `config.baseURL?` - Custom base URL (optional) **Returns:** A Gemini image adapter instance. -### `createGeminiImage(apiKey, config?)` +### `createGeminiImage(model, apiKey, config?)` + +Creates a Gemini image adapter with an explicit API key. -Creates a Gemini image generation adapter with an explicit API key. +**Parameters:** + +- `model` - The model name +- `apiKey` - Your Google API key +- `config.baseURL?` - Custom base URL (optional) **Returns:** A Gemini image adapter instance. @@ -278,6 +380,8 @@ Creates a Gemini TTS adapter with an explicit API key. ## Next Steps +- [Image Generation Guide](../guides/image-generation) - Learn more about image generation +- [Media Generation Example](https://github.com/TanStack/ai/tree/main/examples/ts-react-media) - Full working example with Gemini and fal.ai - [Getting Started](../getting-started/quick-start) - Learn the basics - [Tools Guide](../guides/tools) - Learn about tools - [Other Adapters](./openai) - Explore other providers diff --git a/docs/guides/image-generation.md b/docs/guides/image-generation.md index 1239a1239..5e167f50e 100644 --- a/docs/guides/image-generation.md +++ b/docs/guides/image-generation.md @@ -13,7 +13,8 @@ TanStack AI provides support for image generation through dedicated image adapte Image generation is handled by image adapters that follow the same tree-shakeable architecture as other adapters in TanStack AI. The image adapters support: - **OpenAI**: DALL-E 2, DALL-E 3, GPT-Image-1, and GPT-Image-1-Mini models -- **Gemini**: Imagen 3 and Imagen 4 models +- **Gemini**: Gemini native image models (NanoBanana) and Imagen 3/4 models +- **fal.ai**: 600+ models including Nano Banana Pro, FLUX, and more ## Basic Usage @@ -37,16 +38,22 @@ console.log(result.images[0].url) // URL to the generated image ### Gemini Image Generation +Gemini supports two types of image generation: Gemini native models (NanoBanana) and Imagen models. The adapter automatically routes to the correct API based on the model name. + ```typescript import { generateImage } from '@tanstack/ai' import { geminiImage } from '@tanstack/ai-gemini' -// Create an image adapter (uses GOOGLE_API_KEY from environment) -const adapter = geminiImage() - -// Generate an image +// Gemini native model (NanoBanana) — uses generateContent API const result = await generateImage({ - adapter: geminiImage('imagen-3.0-generate-002'), + adapter: geminiImage('gemini-3.1-flash-image-preview'), + prompt: 'A futuristic cityscape at night', + size: '16:9_4K', +}) + +// Imagen model — uses generateImages API +const result2 = await generateImage({ + adapter: geminiImage('imagen-4.0-generate-001'), prompt: 'A futuristic cityscape at night', }) @@ -78,9 +85,24 @@ All image adapters support these common options: | `dall-e-3` | `1024x1024`, `1792x1024`, `1024x1792` | | `dall-e-2` | `256x256`, `512x512`, `1024x1024` | -#### Gemini Models +#### Gemini Native Models (NanoBanana) -Gemini uses aspect ratios internally, but TanStack AI accepts WIDTHxHEIGHT format and converts them: +Gemini native image models use a template literal size format: `"aspectRatio_resolution"`. + +| Aspect Ratios | Resolutions | +|---------------|-------------| +| `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `9:16`, `16:9`, `21:9` | `1K`, `2K`, `4K` | + +```typescript +// Examples +size: "16:9_4K" // Widescreen at 4K resolution +size: "1:1_2K" // Square at 2K resolution +size: "9:16_1K" // Portrait at 1K resolution +``` + +#### Gemini Imagen Models + +Imagen models accept WIDTHxHEIGHT format, which maps to aspect ratios internally: | Size | Aspect Ratio | |------|-------------| @@ -134,7 +156,7 @@ const result = await generateImage({ }) ``` -### Gemini Model Options +### Gemini Imagen Model Options ```typescript const result = await generateImage({ @@ -150,6 +172,18 @@ const result = await generateImage({ }) ``` +### Gemini Native Model Options (NanoBanana) + +Gemini native image models accept `GenerateContentConfig` options directly in `modelOptions`: + +```typescript +const result = await generateImage({ + adapter: geminiImage('gemini-3.1-flash-image-preview'), + prompt: 'A beautiful garden', + size: '16:9_4K', +}) +``` + ## Response Format The image generation result includes: @@ -184,14 +218,23 @@ interface GeneratedImage { | `dall-e-3` | 1 | | `dall-e-2` | 1-10 | -### Gemini Models +### Gemini Native Models (NanoBanana) + +| Model | Description | +|-------|-------------| +| `gemini-3.1-flash-image-preview` | Latest and fastest Gemini native image generation | +| `gemini-3-pro-image-preview` | Higher quality Gemini native image generation | +| `gemini-2.5-flash-image` | Gemini 2.5 Flash with image generation | +| `gemini-2.0-flash-preview-image-generation` | Gemini 2.0 Flash image generation | + +### Gemini Imagen Models | Model | Images per Request | |-------|-------------------| -| `imagen-3.0-generate-002` | 1-4 | +| `imagen-4.0-ultra-generate-001` | 1-4 | | `imagen-4.0-generate-001` | 1-4 | | `imagen-4.0-fast-generate-001` | 1-4 | -| `imagen-4.0-ultra-generate-001` | 1-4 | +| `imagen-3.0-generate-002` | 1-4 | ## Error Handling @@ -216,7 +259,7 @@ try { The image adapters use the same environment variables as the text adapters: - **OpenAI**: `OPENAI_API_KEY` -- **Gemini**: `GOOGLE_API_KEY` or `GEMINI_API_KEY` +- **Gemini** (including NanoBanana): `GOOGLE_API_KEY` or `GEMINI_API_KEY` ## Explicit API Keys diff --git a/examples/ts-react-fal/.env.example b/examples/ts-react-media/.env.example similarity index 63% rename from examples/ts-react-fal/.env.example rename to examples/ts-react-media/.env.example index ae8401657..b7c897653 100644 --- a/examples/ts-react-fal/.env.example +++ b/examples/ts-react-media/.env.example @@ -1,4 +1,7 @@ -# Duplicate the .env.example file and rename it to .env.local, then add your FAL_KEY. +# Duplicate the .env.example file and rename it to .env.local, then add your API keys. # Sign up for an account at https://fal.ai, and add $20 of credits to your account to get started. FAL_KEY= + +# Get a Google API key at https://aistudio.google.com/apikey +GOOGLE_API_KEY= diff --git a/examples/ts-react-fal/package.json b/examples/ts-react-media/package.json similarity index 92% rename from examples/ts-react-fal/package.json rename to examples/ts-react-media/package.json index 2284e5a94..f5be09f00 100644 --- a/examples/ts-react-fal/package.json +++ b/examples/ts-react-media/package.json @@ -1,5 +1,5 @@ { - "name": "ts-react-fal", + "name": "ts-react-media", "private": true, "type": "module", "scripts": { @@ -13,6 +13,7 @@ "@tailwindcss/vite": "^4.1.18", "@tanstack/ai": "workspace:*", "@tanstack/ai-fal": "workspace:*", + "@tanstack/ai-gemini": "workspace:*", "@tanstack/react-router": "^1.158.4", "@tanstack/react-start": "^1.159.0", "@tanstack/router-plugin": "^1.158.4", diff --git a/examples/ts-react-fal/src/components/Header.tsx b/examples/ts-react-media/src/components/Header.tsx similarity index 91% rename from examples/ts-react-fal/src/components/Header.tsx rename to examples/ts-react-media/src/components/Header.tsx index 6cf2dde8b..f8a33961f 100644 --- a/examples/ts-react-fal/src/components/Header.tsx +++ b/examples/ts-react-media/src/components/Header.tsx @@ -10,7 +10,7 @@ export default function Header() { - Image & Video Generation with fal.ai + Image & Video Generation ) diff --git a/examples/ts-react-fal/src/components/ImageGenerator.tsx b/examples/ts-react-media/src/components/ImageGenerator.tsx similarity index 86% rename from examples/ts-react-fal/src/components/ImageGenerator.tsx rename to examples/ts-react-media/src/components/ImageGenerator.tsx index 9207551a9..484df42c9 100644 --- a/examples/ts-react-fal/src/components/ImageGenerator.tsx +++ b/examples/ts-react-media/src/components/ImageGenerator.tsx @@ -16,6 +16,15 @@ type ModelResult = { error?: string } +function getImageSrc(image: { url?: string; b64Json?: string }): string { + if (image.url) return image.url + if (image.b64Json) return `data:image/png;base64,${image.b64Json}` + return '' +} + +const falModels = IMAGE_MODELS.filter((m) => m.provider === 'fal') +const geminiModels = IMAGE_MODELS.filter((m) => m.provider === 'gemini') + export default function ImageGenerator({ onImageGenerated, }: ImageGeneratorProps) { @@ -50,9 +59,9 @@ export default function ImageGenerator({ ...prev, [model.id]: { status: 'success', result: response }, })) - const imageUrl = response.images[0]?.url - if (imageUrl) { - onImageGenerated?.(imageUrl) + const image = response.images[0] + if (image) { + onImageGenerated?.(getImageSrc(image)) } } catch (err) { setResults((prev) => ({ @@ -77,9 +86,9 @@ export default function ImageGenerator({ data: { prompt, model: selectedModel }, }) setResults({ [selectedModel]: { status: 'success', result: response } }) - const imageUrl = response.images[0]?.url - if (imageUrl) { - onImageGenerated?.(imageUrl) + const image = response.images[0] + if (image) { + onImageGenerated?.(getImageSrc(image)) } } catch (err) { setResults({ @@ -109,11 +118,20 @@ export default function ImageGenerator({ className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50" > - {IMAGE_MODELS.map((model) => ( - - ))} + + {falModels.map((model) => ( + + ))} + + + {geminiModels.map((model) => ( + + ))} + {currentModel && selectedModel !== 'all' && (

@@ -193,7 +211,7 @@ export default function ImageGenerator({ modelResult.result.images.length > 0 && (

{`Generated diff --git a/examples/ts-react-fal/src/components/VideoGenerator.tsx b/examples/ts-react-media/src/components/VideoGenerator.tsx similarity index 100% rename from examples/ts-react-fal/src/components/VideoGenerator.tsx rename to examples/ts-react-media/src/components/VideoGenerator.tsx diff --git a/examples/ts-react-fal/src/lib/models.ts b/examples/ts-react-media/src/lib/models.ts similarity index 65% rename from examples/ts-react-fal/src/lib/models.ts rename to examples/ts-react-media/src/lib/models.ts index 7bd500a84..fedc49a61 100644 --- a/examples/ts-react-fal/src/lib/models.ts +++ b/examples/ts-react-media/src/lib/models.ts @@ -5,6 +5,7 @@ export const IMAGE_MODELS = [ description: 'Fast, high-quality image generation', defaultSize: 'landscape_16_9' as const, sizeType: 'standard' as const, + provider: 'fal' as const, }, { id: 'xai/grok-imagine-image', @@ -12,6 +13,7 @@ export const IMAGE_MODELS = [ description: 'xAI highly aesthetic images with prompt enhancement', defaultSize: '16:9' as const, sizeType: 'aspect_ratio' as const, + provider: 'fal' as const, }, { id: 'fal-ai/flux-2/klein/9b', @@ -19,6 +21,7 @@ export const IMAGE_MODELS = [ description: 'Enhanced realism, crisp text generation', defaultSize: 'landscape_16_9' as const, sizeType: 'standard' as const, + provider: 'fal' as const, }, { id: 'fal-ai/z-image/turbo', @@ -26,6 +29,47 @@ export const IMAGE_MODELS = [ description: 'Super fast 6B parameter model', defaultSize: 'landscape_16_9' as const, sizeType: 'standard' as const, + provider: 'fal' as const, + }, + { + id: 'gemini-3.1-flash-image-preview', + name: 'NanoBanana 2 (Gemini 3.1 Flash)', + description: 'Latest and fastest Gemini native image generation', + defaultSize: '16:9_4K' as const, + sizeType: 'native' as const, + provider: 'gemini' as const, + }, + { + id: 'gemini-3-pro-image-preview', + name: 'NanoBanana Pro (Gemini 3 Pro)', + description: 'Higher quality Gemini native image generation', + defaultSize: '16:9_4K' as const, + sizeType: 'native' as const, + provider: 'gemini' as const, + }, + { + id: 'imagen-4.0-ultra-generate-001', + name: 'Imagen 4.0 Ultra', + description: 'Best quality Imagen image generation', + defaultSize: '1024x1024' as const, + sizeType: 'standard' as const, + provider: 'gemini' as const, + }, + { + id: 'imagen-4.0-generate-001', + name: 'Imagen 4.0', + description: 'High quality Imagen image generation', + defaultSize: '1024x1024' as const, + sizeType: 'standard' as const, + provider: 'gemini' as const, + }, + { + id: 'imagen-4.0-fast-generate-001', + name: 'Imagen 4.0 Fast', + description: 'Fast Imagen image generation', + defaultSize: '1024x1024' as const, + sizeType: 'standard' as const, + provider: 'gemini' as const, }, ] as const diff --git a/examples/ts-react-fal/src/lib/prompts.ts b/examples/ts-react-media/src/lib/prompts.ts similarity index 100% rename from examples/ts-react-fal/src/lib/prompts.ts rename to examples/ts-react-media/src/lib/prompts.ts diff --git a/examples/ts-react-fal/src/lib/server-functions.ts b/examples/ts-react-media/src/lib/server-functions.ts similarity index 82% rename from examples/ts-react-fal/src/lib/server-functions.ts rename to examples/ts-react-media/src/lib/server-functions.ts index d7424504d..1aef006c2 100644 --- a/examples/ts-react-fal/src/lib/server-functions.ts +++ b/examples/ts-react-media/src/lib/server-functions.ts @@ -1,5 +1,6 @@ import { createServerFn } from '@tanstack/react-start' import { falImage, falVideo } from '@tanstack/ai-fal' +import { geminiImage } from '@tanstack/ai-gemini' import { generateImage, generateVideo, getVideoJobStatus } from '@tanstack/ai' import type { FalModel } from '@tanstack/ai-fal' @@ -42,7 +43,7 @@ export const generateImageFn = createServerFn({ method: 'POST' }) adapter: falImage('fal-ai/flux-2/klein/9b'), prompt: data.prompt, numberOfImages: 1, - size: '16:9', + size: 'landscape_16_9', }) } case 'fal-ai/z-image/turbo': { @@ -57,6 +58,46 @@ export const generateImageFn = createServerFn({ method: 'POST' }) }, }) } + case 'gemini-3.1-flash-image-preview': { + return generateImage({ + adapter: geminiImage('gemini-3.1-flash-image-preview'), + prompt: data.prompt, + numberOfImages: 1, + size: '16:9_4K', + }) + } + case 'gemini-3-pro-image-preview': { + return generateImage({ + adapter: geminiImage('gemini-3-pro-image-preview'), + prompt: data.prompt, + numberOfImages: 1, + size: '16:9_4K', + }) + } + case 'imagen-4.0-ultra-generate-001': { + return generateImage({ + adapter: geminiImage('imagen-4.0-ultra-generate-001'), + prompt: data.prompt, + numberOfImages: 1, + size: '1024x1024', + }) + } + case 'imagen-4.0-generate-001': { + return generateImage({ + adapter: geminiImage('imagen-4.0-generate-001'), + prompt: data.prompt, + numberOfImages: 1, + size: '1024x1024', + }) + } + case 'imagen-4.0-fast-generate-001': { + return generateImage({ + adapter: geminiImage('imagen-4.0-fast-generate-001'), + prompt: data.prompt, + numberOfImages: 1, + size: '1024x1024', + }) + } default: throw new Error(`Unknown model: ${data.model}`) } diff --git a/examples/ts-react-fal/src/routeTree.gen.ts b/examples/ts-react-media/src/routeTree.gen.ts similarity index 100% rename from examples/ts-react-fal/src/routeTree.gen.ts rename to examples/ts-react-media/src/routeTree.gen.ts diff --git a/examples/ts-react-fal/src/router.tsx b/examples/ts-react-media/src/router.tsx similarity index 100% rename from examples/ts-react-fal/src/router.tsx rename to examples/ts-react-media/src/router.tsx diff --git a/examples/ts-react-fal/src/routes/__root.tsx b/examples/ts-react-media/src/routes/__root.tsx similarity index 100% rename from examples/ts-react-fal/src/routes/__root.tsx rename to examples/ts-react-media/src/routes/__root.tsx diff --git a/examples/ts-react-fal/src/routes/index.tsx b/examples/ts-react-media/src/routes/index.tsx similarity index 97% rename from examples/ts-react-fal/src/routes/index.tsx rename to examples/ts-react-media/src/routes/index.tsx index 13766747d..496876679 100644 --- a/examples/ts-react-fal/src/routes/index.tsx +++ b/examples/ts-react-media/src/routes/index.tsx @@ -20,7 +20,7 @@ function VisualPage() { Visual Content Generator

- Generate images and videos using fal.ai models + Generate images and videos using AI models

diff --git a/examples/ts-react-fal/src/styles.css b/examples/ts-react-media/src/styles.css similarity index 100% rename from examples/ts-react-fal/src/styles.css rename to examples/ts-react-media/src/styles.css diff --git a/examples/ts-react-fal/tsconfig.json b/examples/ts-react-media/tsconfig.json similarity index 100% rename from examples/ts-react-fal/tsconfig.json rename to examples/ts-react-media/tsconfig.json diff --git a/examples/ts-react-fal/vite.config.ts b/examples/ts-react-media/vite.config.ts similarity index 100% rename from examples/ts-react-fal/vite.config.ts rename to examples/ts-react-media/vite.config.ts diff --git a/packages/typescript/ai-gemini/package.json b/packages/typescript/ai-gemini/package.json index adc204409..77798f0db 100644 --- a/packages/typescript/ai-gemini/package.json +++ b/packages/typescript/ai-gemini/package.json @@ -40,7 +40,7 @@ "adapter" ], "dependencies": { - "@google/genai": "^1.30.0" + "@google/genai": "^1.43.0" }, "peerDependencies": { "@tanstack/ai": "workspace:^" diff --git a/packages/typescript/ai-gemini/src/adapters/image.ts b/packages/typescript/ai-gemini/src/adapters/image.ts index 87c501250..2ccf47b58 100644 --- a/packages/typescript/ai-gemini/src/adapters/image.ts +++ b/packages/typescript/ai-gemini/src/adapters/image.ts @@ -5,6 +5,7 @@ import { getGeminiApiKeyFromEnv, } from '../utils' import { + parseNativeImageSize, sizeToAspectRatio, validateImageSize, validateNumberOfImages, @@ -22,6 +23,8 @@ import type { ImageGenerationResult, } from '@tanstack/ai' import type { + GenerateContentConfig, + GenerateContentResponse, GenerateImagesConfig, GenerateImagesResponse, GoogleGenAI, @@ -39,14 +42,16 @@ export type GeminiImageModel = (typeof GEMINI_IMAGE_MODELS)[number] /** * Gemini Image Generation Adapter * - * Tree-shakeable adapter for Gemini Imagen image generation functionality. - * Supports Imagen 3 and Imagen 4 models. + * Tree-shakeable adapter for Gemini image generation functionality. + * Supports Imagen 3/4 models (via generateImages API) and Gemini native + * image models like Nano Banana 2 (via generateContent API). * * Features: * - Aspect ratio-based image sizing * - Person generation controls * - Safety filtering * - Watermark options + * - Extended resolution tiers (Nano Banana 2) */ export class GeminiImageAdapter< TModel extends GeminiImageModel, @@ -76,15 +81,19 @@ export class GeminiImageAdapter< async generateImages( options: ImageGenerationOptions, ): Promise { - const { model, prompt, numberOfImages, size } = options + const { model, prompt } = options - // Validate inputs validatePrompt({ prompt, model }) - validateImageSize(model, size) - validateNumberOfImages(model, numberOfImages) - // Build request config - const config = this.buildConfig(options) + if (this.isGeminiImageModel(model)) { + return this.generateWithGeminiApi(options) + } + + // Imagen models path (generateImages API) + validateImageSize(model, options.size) + validateNumberOfImages(model, options.numberOfImages) + + const config = this.buildImagenConfig(options) const response = await this.client.models.generateImages({ model, @@ -92,10 +101,78 @@ export class GeminiImageAdapter< config, }) - return this.transformResponse(model, response) + return this.transformImagenResponse(model, response) + } + + private isGeminiImageModel(model: string): boolean { + return model.startsWith('gemini-') + } + + private async generateWithGeminiApi( + options: ImageGenerationOptions, + ): Promise { + const { model, prompt, size, numberOfImages, modelOptions } = options + + const parsedSize = size ? parseNativeImageSize(size) : undefined + + // The generateContent API has no numberOfImages parameter. + // Instead, augment the prompt to request multiple images when needed. + const augmentedPrompt = + numberOfImages && numberOfImages > 1 + ? `${prompt} Generate ${numberOfImages} distinct images.` + : prompt + + const config: GenerateContentConfig = { + // Include TEXT so the model can interleave descriptions between images + responseModalities: ['TEXT', 'IMAGE'], + ...(parsedSize && { + imageConfig: { + ...(parsedSize.aspectRatio && { + aspectRatio: parsedSize.aspectRatio, + }), + ...(parsedSize.resolution && { + imageSize: parsedSize.resolution, + }), + }, + }), + ...modelOptions, + } + + const response = await this.client.models.generateContent({ + model, + contents: augmentedPrompt, + config, + }) + + return this.transformGeminiResponse(model, response) + } + + private transformGeminiResponse( + model: string, + response: GenerateContentResponse, + ): ImageGenerationResult { + const images: Array = [] + const parts = response.candidates?.[0]?.content?.parts ?? [] + + for (const part of parts) { + if ( + part.inlineData?.data && + typeof part.inlineData.data === 'string' && + part.inlineData.data.length > 0 + ) { + images.push({ b64Json: part.inlineData.data }) + } + } + + return { + id: generateId(this.name), + model, + images, + usage: undefined, + } } - private buildConfig( + private buildImagenConfig( options: ImageGenerationOptions, ): GenerateImagesConfig { const { size, numberOfImages, modelOptions } = options @@ -108,7 +185,7 @@ export class GeminiImageAdapter< } } - private transformResponse( + private transformImagenResponse( model: string, response: GenerateImagesResponse, ): ImageGenerationResult { diff --git a/packages/typescript/ai-gemini/src/image/image-provider-options.ts b/packages/typescript/ai-gemini/src/image/image-provider-options.ts index 2da393712..6574ed76c 100644 --- a/packages/typescript/ai-gemini/src/image/image-provider-options.ts +++ b/packages/typescript/ai-gemini/src/image/image-provider-options.ts @@ -145,11 +145,49 @@ export type GeminiImageSize = | '1080x1920' /** - * Model-specific size options mapping - * All Imagen models use the same size options + * Aspect ratios supported by Gemini native image models (via generateContent API). + * Matches the SDK's ImageConfig.aspectRatio values. + */ +export type GeminiNativeImageAspectRatio = + | '1:1' + | '2:3' + | '3:2' + | '3:4' + | '4:3' + | '9:16' + | '16:9' + | '21:9' + +/** + * Resolution tiers for Gemini native image models. + * Matches the SDK's ImageConfig.imageSize values. + */ +export type GeminiNativeImageResolution = '1K' | '2K' | '4K' + +/** + * Template literal size type for Gemini native image models: "16:9_4K", "1:1_2K", etc. + */ +export type GeminiNativeImageSize = + `${GeminiNativeImageAspectRatio}_${GeminiNativeImageResolution}` + +/** + * Gemini native image models that use the generateContent API path. + * These models support template literal sizes (aspectRatio_resolution). + */ +export type GeminiNativeImageModels = + | 'gemini-3.1-flash-image-preview' + | 'gemini-3-pro-image-preview' + | 'gemini-2.5-flash-image' + | 'gemini-2.0-flash-preview-image-generation' + +/** + * Model-specific size options mapping. + * Gemini native image models use template literal sizes, Imagen models use pixel sizes. */ export type GeminiImageModelSizeByName = { - [K in GeminiImageModels]: GeminiImageSize + [K in GeminiNativeImageModels]: GeminiNativeImageSize +} & { + [K in Exclude]: GeminiImageSize } /** @@ -237,3 +275,15 @@ export function validatePrompt(options: { throw new Error(`Prompt cannot be empty for model "${model}".`) } } + +/** + * Parses a Gemini native image size string into its components. + * Format: "aspectRatio_resolution" e.g. "16:9_4K" → { aspectRatio: "16:9", resolution: "4K" } + */ +export function parseNativeImageSize( + size: string, +): { aspectRatio: string; resolution: string } | undefined { + const match = size.match(/^(\d+:\d+)_(.+)$/) + if (!match) return undefined + return { aspectRatio: match[1]!, resolution: match[2]! } +} diff --git a/packages/typescript/ai-gemini/src/model-meta.ts b/packages/typescript/ai-gemini/src/model-meta.ts index 54761a026..c8a187f26 100644 --- a/packages/typescript/ai-gemini/src/model-meta.ts +++ b/packages/typescript/ai-gemini/src/model-meta.ts @@ -47,6 +47,44 @@ interface ModelMeta { providerOptions?: TProviderOptions } +const GEMINI_3_1_PRO = { + name: 'gemini-3.1-pro-preview', + max_input_tokens: 1_048_576, + max_output_tokens: 65_536, + knowledge_cutoff: '2025-01-01', + supports: { + input: ['text', 'image', 'audio', 'video', 'document'], + output: ['text'], + capabilities: [ + 'batch_api', + 'caching', + 'code_execution', + 'file_search', + 'function_calling', + 'search_grounding', + 'structured_output', + 'thinking', + 'url_context', + ], + }, + pricing: { + input: { + normal: 2.5, + }, + output: { + normal: 15, + }, + }, +} as const satisfies ModelMeta< + GeminiToolConfigOptions & + GeminiSafetyOptions & + GeminiCommonConfigOptions & + GeminiCachedContentOptions & + GeminiStructuredOutputOptions & + GeminiThinkingOptions & + GeminiThinkingAdvancedOptions +> + const GEMINI_3_PRO = { name: 'gemini-3-pro-preview', max_input_tokens: 1_048_576, @@ -157,6 +195,39 @@ const GEMINI_3_PRO_IMAGE = { GeminiThinkingAdvancedOptions > +const GEMINI_3_1_FLASH_IMAGE = { + name: 'gemini-3.1-flash-image-preview', + max_input_tokens: 65_536, + max_output_tokens: 65_536, + knowledge_cutoff: '2025-01-01', + supports: { + input: ['text', 'image'], + output: ['text', 'image'], + capabilities: [ + 'batch_api', + 'image_generation', + 'search_grounding', + 'structured_output', + 'thinking', + ], + }, + pricing: { + input: { + normal: 0.25, + }, + output: { + normal: 1.5, + }, + }, +} as const satisfies ModelMeta< + GeminiToolConfigOptions & + GeminiSafetyOptions & + GeminiCommonConfigOptions & + GeminiCachedContentOptions & + GeminiStructuredOutputOptions & + GeminiThinkingOptions +> + const GEMINI_2_5_PRO = { name: 'gemini-2.5-pro', max_input_tokens: 1_048_576, @@ -820,6 +891,7 @@ const VEO_2 = { } as const */ export const GEMINI_MODELS = [ + GEMINI_3_1_PRO.name, GEMINI_3_PRO.name, GEMINI_3_FLASH.name, GEMINI_2_5_PRO.name, @@ -836,6 +908,7 @@ export type GeminiModels = (typeof GEMINI_MODELS)[number] export type GeminiImageModels = (typeof GEMINI_IMAGE_MODELS)[number] export const GEMINI_IMAGE_MODELS = [ + GEMINI_3_1_FLASH_IMAGE.name, GEMINI_3_PRO_IMAGE.name, GEMINI_2_5_FLASH_IMAGE.name, GEMINI_2_FLASH_IMAGE.name, @@ -911,6 +984,13 @@ export type GeminiTTSVoice = (typeof GEMINI_TTS_VOICES)[number] // Manual type map for per-model provider options export type GeminiChatModelProviderOptionsByName = { // Models with thinking and structured output support + [GEMINI_3_1_PRO.name]: GeminiToolConfigOptions & + GeminiSafetyOptions & + GeminiCommonConfigOptions & + GeminiCachedContentOptions & + GeminiStructuredOutputOptions & + GeminiThinkingOptions & + GeminiThinkingAdvancedOptions [GEMINI_3_PRO.name]: GeminiToolConfigOptions & GeminiSafetyOptions & GeminiCommonConfigOptions & @@ -983,6 +1063,7 @@ export type GeminiChatModelProviderOptionsByName = { */ export type GeminiModelInputModalitiesByName = { // Models with full multimodal support (text, image, audio, video, document) + [GEMINI_3_1_PRO.name]: typeof GEMINI_3_1_PRO.supports.input [GEMINI_3_PRO.name]: typeof GEMINI_3_PRO.supports.input [GEMINI_3_FLASH.name]: typeof GEMINI_3_FLASH.supports.input [GEMINI_2_5_PRO.name]: typeof GEMINI_2_5_PRO.supports.input diff --git a/packages/typescript/ai-gemini/tests/image-adapter.test.ts b/packages/typescript/ai-gemini/tests/image-adapter.test.ts index b990fdc7e..9ca3af076 100644 --- a/packages/typescript/ai-gemini/tests/image-adapter.test.ts +++ b/packages/typescript/ai-gemini/tests/image-adapter.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi } from 'vitest' import { GeminiImageAdapter, createGeminiImage } from '../src/adapters/image' import { + parseNativeImageSize, sizeToAspectRatio, validateImageSize, validateNumberOfImages, @@ -115,8 +116,32 @@ describe('Gemini Image Adapter', () => { }) }) + describe('parseNativeImageSize', () => { + it('parses template literal sizes into components', () => { + expect(parseNativeImageSize('16:9_4K')).toEqual({ + aspectRatio: '16:9', + resolution: '4K', + }) + expect(parseNativeImageSize('1:1_2K')).toEqual({ + aspectRatio: '1:1', + resolution: '2K', + }) + expect(parseNativeImageSize('21:9_1K')).toEqual({ + aspectRatio: '21:9', + resolution: '1K', + }) + }) + + it('returns undefined for invalid formats', () => { + expect(parseNativeImageSize('1024x1024')).toBeUndefined() + expect(parseNativeImageSize('invalid')).toBeUndefined() + expect(parseNativeImageSize('16:9')).toBeUndefined() + expect(parseNativeImageSize('4K')).toBeUndefined() + }) + }) + describe('generateImages', () => { - it('calls the Gemini models.generateImages API', async () => { + it('calls the Gemini models.generateImages API for Imagen models', async () => { const mockResponse = { generatedImages: [ { @@ -129,7 +154,10 @@ describe('Gemini Image Adapter', () => { const mockGenerateImages = vi.fn().mockResolvedValueOnce(mockResponse) - const adapter = createGeminiImage('test-api-key') + const adapter = createGeminiImage( + 'imagen-3.0-generate-002', + 'test-api-key', + ) // Replace the internal Gemini SDK client with our mock ;( adapter as unknown as { @@ -169,7 +197,10 @@ describe('Gemini Image Adapter', () => { const mockGenerateImages = vi.fn().mockResolvedValue(mockResponse) - const adapter = createGeminiImage('test-api-key') + const adapter = createGeminiImage( + 'imagen-3.0-generate-002', + 'test-api-key', + ) ;( adapter as unknown as { client: { models: { generateImages: unknown } } @@ -194,5 +225,295 @@ describe('Gemini Image Adapter', () => { expect(result1.id).toMatch(/^gemini-/) expect(result2.id).toMatch(/^gemini-/) }) + + it('calls generateContent API for Gemini image models', async () => { + const mockResponse = { + candidates: [ + { + content: { + parts: [ + { + inlineData: { + mimeType: 'image/png', + data: 'gemini-base64-image', + }, + }, + ], + }, + }, + ], + } + + const mockGenerateContent = vi.fn().mockResolvedValueOnce(mockResponse) + + const adapter = createGeminiImage( + 'gemini-3.1-flash-image-preview', + 'test-api-key', + ) + ;( + adapter as unknown as { + client: { models: { generateContent: unknown } } + } + ).client = { + models: { + generateContent: mockGenerateContent, + }, + } + + const result = await adapter.generateImages({ + model: 'gemini-3.1-flash-image-preview', + prompt: 'A futuristic city', + size: '16:9_4K', + }) + + expect(mockGenerateContent).toHaveBeenCalledWith({ + model: 'gemini-3.1-flash-image-preview', + contents: 'A futuristic city', + config: { + responseModalities: ['TEXT', 'IMAGE'], + imageConfig: { + aspectRatio: '16:9', + imageSize: '4K', + }, + }, + }) + + expect(result.model).toBe('gemini-3.1-flash-image-preview') + expect(result.images).toHaveLength(1) + expect(result.images[0].b64Json).toBe('gemini-base64-image') + }) + + it('calls generateContent without imageConfig when no size provided', async () => { + const mockResponse = { + candidates: [ + { + content: { + parts: [ + { + inlineData: { + mimeType: 'image/png', + data: 'gemini-base64-image', + }, + }, + ], + }, + }, + ], + } + + const mockGenerateContent = vi.fn().mockResolvedValueOnce(mockResponse) + + const adapter = createGeminiImage( + 'gemini-3.1-flash-image-preview', + 'test-api-key', + ) + ;( + adapter as unknown as { + client: { models: { generateContent: unknown } } + } + ).client = { + models: { + generateContent: mockGenerateContent, + }, + } + + const result = await adapter.generateImages({ + model: 'gemini-3.1-flash-image-preview', + prompt: 'A simple sketch', + }) + + expect(mockGenerateContent).toHaveBeenCalledWith({ + model: 'gemini-3.1-flash-image-preview', + contents: 'A simple sketch', + config: { + responseModalities: ['TEXT', 'IMAGE'], + }, + }) + + expect(result.images).toHaveLength(1) + }) + + it('handles empty response from Gemini image model', async () => { + const mockResponse = { + candidates: [ + { + content: { + parts: [], + }, + }, + ], + } + + const mockGenerateContent = vi.fn().mockResolvedValueOnce(mockResponse) + + const adapter = createGeminiImage( + 'gemini-3.1-flash-image-preview', + 'test-api-key', + ) + ;( + adapter as unknown as { + client: { models: { generateContent: unknown } } + } + ).client = { + models: { + generateContent: mockGenerateContent, + }, + } + + const result = await adapter.generateImages({ + model: 'gemini-3.1-flash-image-preview', + prompt: 'A test prompt', + }) + + expect(result.images).toHaveLength(0) + }) + + it('augments prompt when numberOfImages > 1 for Gemini models', async () => { + const mockResponse = { + candidates: [ + { + content: { + parts: [ + { + inlineData: { mimeType: 'image/png', data: 'img1' }, + }, + { + text: 'Here is the second image:', + }, + { + inlineData: { mimeType: 'image/png', data: 'img2' }, + }, + ], + }, + }, + ], + } + + const mockGenerateContent = vi.fn().mockResolvedValueOnce(mockResponse) + + const adapter = createGeminiImage( + 'gemini-3.1-flash-image-preview', + 'test-api-key', + ) + ;( + adapter as unknown as { + client: { models: { generateContent: unknown } } + } + ).client = { + models: { + generateContent: mockGenerateContent, + }, + } + + const result = await adapter.generateImages({ + model: 'gemini-3.1-flash-image-preview', + prompt: 'A futuristic city', + numberOfImages: 3, + }) + + expect(mockGenerateContent).toHaveBeenCalledWith({ + model: 'gemini-3.1-flash-image-preview', + contents: 'A futuristic city Generate 3 distinct images.', + config: { + responseModalities: ['TEXT', 'IMAGE'], + }, + }) + + // Collects all inlineData parts, skipping text parts + expect(result.images).toHaveLength(2) + expect(result.images[0].b64Json).toBe('img1') + expect(result.images[1].b64Json).toBe('img2') + }) + + it('does not augment prompt when numberOfImages is 1', async () => { + const mockResponse = { + candidates: [ + { + content: { + parts: [ + { + inlineData: { mimeType: 'image/png', data: 'img1' }, + }, + ], + }, + }, + ], + } + + const mockGenerateContent = vi.fn().mockResolvedValueOnce(mockResponse) + + const adapter = createGeminiImage( + 'gemini-3.1-flash-image-preview', + 'test-api-key', + ) + ;( + adapter as unknown as { + client: { models: { generateContent: unknown } } + } + ).client = { + models: { + generateContent: mockGenerateContent, + }, + } + + await adapter.generateImages({ + model: 'gemini-3.1-flash-image-preview', + prompt: 'A simple sketch', + numberOfImages: 1, + }) + + expect(mockGenerateContent).toHaveBeenCalledWith({ + model: 'gemini-3.1-flash-image-preview', + contents: 'A simple sketch', + config: { + responseModalities: ['TEXT', 'IMAGE'], + }, + }) + }) + + it('does not augment prompt when numberOfImages is undefined', async () => { + const mockResponse = { + candidates: [ + { + content: { + parts: [ + { + inlineData: { mimeType: 'image/png', data: 'img1' }, + }, + ], + }, + }, + ], + } + + const mockGenerateContent = vi.fn().mockResolvedValueOnce(mockResponse) + + const adapter = createGeminiImage( + 'gemini-3.1-flash-image-preview', + 'test-api-key', + ) + ;( + adapter as unknown as { + client: { models: { generateContent: unknown } } + } + ).client = { + models: { + generateContent: mockGenerateContent, + }, + } + + await adapter.generateImages({ + model: 'gemini-3.1-flash-image-preview', + prompt: 'A simple sketch', + }) + + expect(mockGenerateContent).toHaveBeenCalledWith({ + model: 'gemini-3.1-flash-image-preview', + contents: 'A simple sketch', + config: { + responseModalities: ['TEXT', 'IMAGE'], + }, + }) + }) }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b669fda9..6e975a75c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -326,7 +326,7 @@ importers: specifier: ^5.1.0 version: 5.1.0 - examples/ts-react-fal: + examples/ts-react-media: dependencies: '@tailwindcss/vite': specifier: ^4.1.18 @@ -337,6 +337,9 @@ importers: '@tanstack/ai-fal': specifier: workspace:* version: link:../../packages/typescript/ai-fal + '@tanstack/ai-gemini': + specifier: workspace:* + version: link:../../packages/typescript/ai-gemini '@tanstack/react-router': specifier: ^1.158.4 version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -762,8 +765,8 @@ importers: packages/typescript/ai-gemini: dependencies: '@google/genai': - specifier: ^1.30.0 - version: 1.33.0 + specifier: ^1.43.0 + version: 1.43.0 devDependencies: '@tanstack/ai': specifier: workspace:* @@ -2387,11 +2390,11 @@ packages: '@gerrit0/mini-shiki@3.19.0': resolution: {integrity: sha512-ZSlWfLvr8Nl0T4iA3FF/8VH8HivYF82xQts2DY0tJxZd4wtXJ8AA0nmdW9lmO4hlrh3f9xNwEPtOgqETPqKwDA==} - '@google/genai@1.33.0': - resolution: {integrity: sha512-ThUjFZ1N0DU88peFjnQkb8K198EWaW2RmmnDShFQ+O+xkIH9itjpRe358x3L/b4X/A7dimkvq63oz49Vbh7Cog==} + '@google/genai@1.43.0': + resolution: {integrity: sha512-hklCsJNdMlDM1IwcCVcGQFBg2izY0+t5BIGbRsxi2UnKi6AGKL7pqJqmBDNRbw0bYCs4y3NA7TB+fkKfP/Nrdw==} engines: {node: '>=20.0.0'} peerDependencies: - '@modelcontextprotocol/sdk': ^1.24.0 + '@modelcontextprotocol/sdk': ^1.25.2 peerDependenciesMeta: '@modelcontextprotocol/sdk': optional: true @@ -3073,6 +3076,36 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@publint/pack@0.1.2': resolution: {integrity: sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==} engines: {node: '>=18'} @@ -4411,6 +4444,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -6103,9 +6139,6 @@ packages: h3@1.13.0: resolution: {integrity: sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==} - h3@1.15.4: - resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} - h3@1.15.5: resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} @@ -6796,6 +6829,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -7388,6 +7424,10 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -7577,6 +7617,10 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -7758,6 +7802,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -8565,68 +8613,6 @@ packages: synckit: optional: true - unstorage@1.17.3: - resolution: {integrity: sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==} - peerDependencies: - '@azure/app-configuration': ^1.8.0 - '@azure/cosmos': ^4.2.0 - '@azure/data-tables': ^13.3.0 - '@azure/identity': ^4.6.0 - '@azure/keyvault-secrets': ^4.9.0 - '@azure/storage-blob': ^12.26.0 - '@capacitor/preferences': ^6.0.3 || ^7.0.0 - '@deno/kv': '>=0.9.0' - '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 - '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.34.3 - '@vercel/blob': '>=0.27.1' - '@vercel/functions': ^2.2.12 || ^3.0.0 - '@vercel/kv': ^1.0.1 - aws4fetch: ^1.0.20 - db0: '>=0.2.1' - idb-keyval: ^6.2.1 - ioredis: ^5.4.2 - uploadthing: ^7.4.4 - peerDependenciesMeta: - '@azure/app-configuration': - optional: true - '@azure/cosmos': - optional: true - '@azure/data-tables': - optional: true - '@azure/identity': - optional: true - '@azure/keyvault-secrets': - optional: true - '@azure/storage-blob': - optional: true - '@capacitor/preferences': - optional: true - '@deno/kv': - optional: true - '@netlify/blobs': - optional: true - '@planetscale/database': - optional: true - '@upstash/redis': - optional: true - '@vercel/blob': - optional: true - '@vercel/functions': - optional: true - '@vercel/kv': - optional: true - aws4fetch: - optional: true - db0: - optional: true - idb-keyval: - optional: true - ioredis: - optional: true - uploadthing: - optional: true - unstorage@1.17.4: resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} peerDependencies: @@ -10098,9 +10084,11 @@ snapshots: '@shikijs/types': 3.20.0 '@shikijs/vscode-textmate': 10.0.2 - '@google/genai@1.33.0': + '@google/genai@1.43.0': dependencies: google-auth-library: 10.5.0 + p-retry: 4.6.2 + protobufjs: 7.5.4 ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -10636,6 +10624,29 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@publint/pack@0.1.2': {} '@quansync/fs@1.0.0': @@ -12695,6 +12706,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/retry@0.12.0': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -14778,18 +14791,6 @@ snapshots: uncrypto: 0.1.3 unenv: 1.10.0 - h3@1.15.4: - dependencies: - cookie-es: 1.2.2 - crossws: 0.3.5 - defu: 6.1.4 - destr: 2.0.5 - iron-webcrypto: 1.2.1 - node-mock-http: 1.0.4 - radix3: 1.1.2 - ufo: 1.6.1 - uncrypto: 0.1.3 - h3@1.15.5: dependencies: cookie-es: 1.2.2 @@ -15546,6 +15547,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.3.2: {} + longest-streak@3.1.0: {} lowlight@3.3.0: @@ -16152,7 +16155,7 @@ snapshots: exsolve: 1.0.8 globby: 15.0.0 gzip-size: 7.0.0 - h3: 1.15.4 + h3: 1.15.5 hookable: 5.5.3 httpxy: 0.1.7 ioredis: 5.8.2 @@ -16188,7 +16191,7 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 5.5.0 unplugin-utils: 0.3.1 - unstorage: 1.17.3(db0@0.3.4)(ioredis@5.8.2) + unstorage: 1.17.4(db0@0.3.4)(ioredis@5.8.2) untyped: 2.0.0 unwasm: 0.3.11 youch: 4.1.0-beta.13 @@ -16610,6 +16613,11 @@ snapshots: p-map@2.1.0: {} + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -16768,6 +16776,21 @@ snapshots: proto-list@1.2.4: {} + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 24.10.3 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -16993,6 +17016,8 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + retry@0.13.1: {} + reusify@1.1.0: {} rimraf@5.0.10: @@ -17953,20 +17978,6 @@ snapshots: dependencies: rolldown: 1.0.0-beta.53 - unstorage@1.17.3(db0@0.3.4)(ioredis@5.8.2): - dependencies: - anymatch: 3.1.3 - chokidar: 4.0.3 - destr: 2.0.5 - h3: 1.15.5 - lru-cache: 10.4.3 - node-fetch-native: 1.6.7 - ofetch: 1.5.1 - ufo: 1.6.1 - optionalDependencies: - db0: 0.3.4 - ioredis: 5.8.2 - unstorage@1.17.4(db0@0.3.4)(ioredis@5.8.2): dependencies: anymatch: 3.1.3