From b61448c8b7bda8d2a2ebd6360a9525017f39d8f7 Mon Sep 17 00:00:00 2001 From: 404oops Date: Sun, 11 Oct 2026 23:31:41 +0200 Subject: [PATCH] feat(ai-image): add Gemini Nano Banana 2.1 Catalog gemini-nano-banana-2.1 (aliases google/gemini-nano-banana-2.1 and nano-banana-2.1) at Google's standard rates: $1.50/1M input, $7.50/1M text and thinking output, $30/1M image output, with 1K/2K/4K tiers at 1120/1680/3780 image tokens. 3.1 Flash Image stays the provider default. Pre-flight credit checks assumed 50 text/thinking output tokens per image for every Gemini model; live samples run 270-1122 depending on the model, so each model now carries its own estimate. Co-Authored-By: Claude Opus 5.5 --- .../gemini/GeminiImageProvider.test.ts | 128 ++++++++++++++++++ .../providers/gemini/GeminiImageProvider.ts | 5 +- .../ai-image/providers/gemini/models.ts | 52 ++++++- src/docs/src/AI/txt2img.md | 12 +- 4 files changed, 183 insertions(+), 14 deletions(-) diff --git a/src/backend/drivers/ai-image/providers/gemini/GeminiImageProvider.test.ts b/src/backend/drivers/ai-image/providers/gemini/GeminiImageProvider.test.ts index 7a25113de..3a1fde127 100644 --- a/src/backend/drivers/ai-image/providers/gemini/GeminiImageProvider.test.ts +++ b/src/backend/drivers/ai-image/providers/gemini/GeminiImageProvider.test.ts @@ -550,3 +550,131 @@ describe('Gemini image standard pricing', () => { ])); }); }); + +describe('Gemini Nano Banana image generation', () => { + it.each([ + ['gemini-nano-banana-2.1', '1K', 1200], + ['google/gemini-nano-banana-2.1', '2K', 1680], + ['nano-banana-2.1', '4K', 3780], + ])( + 'routes %s and meters %s image output', + async (model, quality, imageTokens) => { + generateContentMock.mockResolvedValueOnce({ + candidates: [ + { + content: { + parts: [ + { + inlineData: { + mimeType: 'image/png', + data: 'AAAA', + }, + }, + ], + }, + }, + ], + usageMetadata: { + promptTokenCount: 100, + candidatesTokenCount: imageTokens + 80, + candidatesTokensDetails: [ + { modality: 'IMAGE', tokenCount: imageTokens }, + { modality: 'TEXT', tokenCount: 80 }, + ], + thoughtsTokenCount: 20, + }, + }); + const result = await withTestActor(() => + makeProvider().generate({ model, quality, prompt: 'A cup' }), + ); + expect(result).toBe('data:image/png;base64,AAAA'); + expect(generateContentMock).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'gemini-nano-banana-2.1', + config: expect.objectContaining({ + imageConfig: { aspectRatio: '1:1', imageSize: quality }, + }), + }), + ); + expect(batchIncrementUsagesSpy).toHaveBeenCalledWith( + expect.anything(), + expect.arrayContaining([ + expect.objectContaining({ + usageType: 'gemini:gemini-nano-banana-2.1:input', + costOverride: 15000, + }), + expect.objectContaining({ + usageType: 'gemini:gemini-nano-banana-2.1:output:text', + costOverride: 75000, + }), + expect.objectContaining({ + usageType: 'gemini:gemini-nano-banana-2.1:output:image', + costOverride: imageTokens * 3000, + }), + ]), + ); + }, + ); + + it('defaults to 1K and preserves input image editing', async () => { + generateContentMock.mockResolvedValueOnce({ + candidates: [ + { + content: { + parts: [ + { + inlineData: { + mimeType: 'image/png', + data: 'AAAA', + }, + }, + ], + }, + }, + ], + }); + await withTestActor(() => + makeProvider().generate({ + model: 'gemini-nano-banana-2.1', + prompt: 'Make it blue', + input_image: 'data:image/png;base64,AAAA', + }), + ); + const request = generateContentMock.mock.calls[0][0]; + expect(request.config.imageConfig.imageSize).toBe('1K'); + // Input + 1120 image tokens at $30/1M + 1000 text tokens at $7.50/1M. + expect(hasCreditsSpy).toHaveBeenCalledWith(expect.anything(), 4_194_450); + expect(batchIncrementUsagesSpy).toHaveBeenCalledWith( + expect.anything(), + expect.arrayContaining([ + expect.objectContaining({ + usageType: 'gemini:gemini-nano-banana-2.1:output:image', + usageAmount: 1120, + costOverride: 3_360_000, + }), + ]), + ); + expect(request.contents).toContainEqual({ + inlineData: { mimeType: 'image/png', data: 'AAAA' }, + }); + }); + + it('rejects 512px output before making a provider request', async () => { + await expect( + withTestActor(() => + makeProvider().generate({ + model: 'gemini-nano-banana-2.1', + prompt: 'A cup', + quality: '512', + }), + ), + ).rejects.toMatchObject({ + message: expect.stringContaining('Unsupported quality tier'), + }); + expect(generateContentMock).not.toHaveBeenCalled(); + }); + + it('keeps the existing provider default', () => { + expect(makeProvider().getDefaultModel()).toBe('gemini-3.1-flash-image'); + }); +}); diff --git a/src/backend/drivers/ai-image/providers/gemini/GeminiImageProvider.ts b/src/backend/drivers/ai-image/providers/gemini/GeminiImageProvider.ts index e59f46a58..b5d3f9b64 100644 --- a/src/backend/drivers/ai-image/providers/gemini/GeminiImageProvider.ts +++ b/src/backend/drivers/ai-image/providers/gemini/GeminiImageProvider.ts @@ -25,6 +25,7 @@ import type { AiMeteringService } from '../../../util/aiCostFactor.js'; import { GEMINI_DEFAULT_RATIO, GEMINI_ESTIMATED_IMAGE_TOKENS, + GEMINI_ESTIMATED_TEXT_OUTPUT_TOKENS, GEMINI_IMAGE_GENERATION_MODELS, } from './models.js'; import type { @@ -188,9 +189,9 @@ export class GeminiImageProvider implements IImageProvider { selectedModel.costs.output_image, ); const estimatedOutputTextCostInCents = this.#calculateTokenCostInCents( - 50, + GEMINI_ESTIMATED_TEXT_OUTPUT_TOKENS[selectedModel.id] ?? 50, selectedModel.costs.output, - ); // small text overhead estimate + ); const estimatedOutputCostInCents = estimatedOutputImageCostInCents + estimatedOutputTextCostInCents; diff --git a/src/backend/drivers/ai-image/providers/gemini/models.ts b/src/backend/drivers/ai-image/providers/gemini/models.ts index b40a20df7..7d82c12d4 100644 --- a/src/backend/drivers/ai-image/providers/gemini/models.ts +++ b/src/backend/drivers/ai-image/providers/gemini/models.ts @@ -24,7 +24,12 @@ export const GEMINI_DEFAULT_RATIO = { w: 1024, h: 1024 }; // Estimated image output token counts for pre-flight cost checks. // These are based on Google's published pricing equivalences. // https://ai.google.dev/gemini-api/docs/image-generation#aspect_ratios_and_image_size +// https://ai.google.dev/gemini-api/docs/pricing export const GEMINI_ESTIMATED_IMAGE_TOKENS: Record = { + 'gemini-nano-banana-2.1:1K': 1120, + 'gemini-nano-banana-2.1:2K': 1680, + 'gemini-nano-banana-2.1:4K': 3780, + 'gemini-3-pro-image:1K': 1120, 'gemini-3-pro-image:2K': 1120, 'gemini-3-pro-image:4K': 2000, @@ -37,10 +42,20 @@ export const GEMINI_ESTIMATED_IMAGE_TOKENS: Record = { 'gemini-3.1-flash-lite-image:1K': 1120, }; +// Estimated text and thinking output tokens per image for pre-flight cost +// checks. Google bills these at the model's text output rate; samples taken +// 2026-10-11 ran 380-425 on 3.1 Flash, about 400 on 3.1 Flash Lite, 270 on +// 3 Pro, and 440-1122 on Nano Banana 2.1. +export const GEMINI_ESTIMATED_TEXT_OUTPUT_TOKENS: Record = { + 'gemini-nano-banana-2.1': 1000, + 'gemini-3-pro-image': 300, + 'gemini-3.1-flash-image': 450, + 'gemini-3.1-flash-lite-image': 450, +}; + export const GEMINI_IMAGE_GENERATION_MODELS: IImageModel[] = [ { - // First entry is the provider default: Google's recommended model and - // the named replacement for the retiring 2.5 Flash Image. + // Keep the existing provider default for callers that omit a model. puterId: 'google:google/gemini-3.1-flash-image', id: 'gemini-3.1-flash-image', name: 'Gemini 3.1 Flash Image', @@ -78,6 +93,39 @@ export const GEMINI_IMAGE_GENERATION_MODELS: IImageModel[] = [ '1K:1x1': 6.7, }, }, + { + puterId: 'google:google/gemini-nano-banana-2.1', + id: 'gemini-nano-banana-2.1', + name: 'Gemini Nano Banana 2.1', + version: '1.0', + costs_currency: 'usd-cents', + index_cost_key: '1K:1x1', + index_input_cost_key: 'input', + aliases: ['google/gemini-nano-banana-2.1', 'nano-banana-2.1'], + allowedQualityLevels: ['1K', '2K', '4K'], + allowedRatios: [ + { w: 1, h: 1 }, + { w: 1, h: 4 }, + { w: 1, h: 8 }, + { w: 2, h: 3 }, + { w: 3, h: 2 }, + { w: 3, h: 4 }, + { w: 4, h: 1 }, + { w: 4, h: 3 }, + { w: 4, h: 5 }, + { w: 5, h: 4 }, + { w: 8, h: 1 }, + { w: 9, h: 16 }, + { w: 16, h: 9 }, + { w: 21, h: 9 }, + ], + costs: { + input: 150, // $1.50 per 1M input tokens (text/image) + output: 750, // $7.50 per 1M output tokens (text and thinking) + output_image: 3000, // $30.00 per 1M output image tokens + '1K:1x1': 3.36, + }, + }, { puterId: 'google:google/gemini-3.1-flash-lite-image', id: 'gemini-3.1-flash-lite-image', diff --git a/src/docs/src/AI/txt2img.md b/src/docs/src/AI/txt2img.md index 1ae674d70..79ef3d248 100755 --- a/src/docs/src/AI/txt2img.md +++ b/src/docs/src/AI/txt2img.md @@ -69,14 +69,6 @@ See the [AI model directory](https://developer.puter.com/ai/models/) for the inc `puter.ai.listModels()` lists chat models. The image catalog above describes configured integrations; upstream account access and availability can still vary. -**Availability notes (September 30, 2026):** - -- Gemini `gemini-3-pro-image` and `gemini-3.1-flash-image` use stable endpoints. Their previous `-preview` spellings remain aliases. Google deprecated `gemini-2.5-flash-image` (shutdown October 2, 2026); Puter no longer offers it, and requests for `gemini-2.5-flash-image` or `nano-banana` fail as unknown models. Use `gemini-3.1-flash-image`, the default here. See [Google's deprecation schedule](https://ai.google.dev/gemini-api/docs/deprecations). -- OpenAI has deprecated `gpt-image-1`, `gpt-image-1-mini`, and `gpt-image-1.5`. Puter no longer offers them through any provider; requests for them fail with `Model not found`. Use `gpt-image-2` or a `gpt-image-2.5-*` model. `chatgpt-image-latest` was never offered. See [OpenAI's deprecation schedule](https://developers.openai.com/api/docs/deprecations). -- All cataloged Together image routes are excluded because they require a third-party data-sharing opt-in. This includes Imagen 4; its retirement through Together has not been confirmed. Excluded routes reject generation before contacting the provider. Previously retired Together aliases still fail as unavailable. To use Cloudflare Schnell, select `@cf/black-forest-labs/flux-1-schnell` or `workers-ai:black-forest-labs/flux.1-schnell`. -- Cloudflare `@cf/black-forest-labs/flux-2-dev` timed out on every live request on September 16, 2026, including 512×512 at 4 steps, while the Klein variants respond normally. It stays listed pending a Cloudflare-side fix. -- xAI `grok-imagine-image-2.0` is available. xAI retires `grok-imagine-image-quality` on November 2, 2026; Puter no longer offers it, and requests for it fail as an unknown model. Use `grok-imagine-image-2.0`. The original `grok-imagine-image` remains available. See [xAI's release notes](https://docs.x.ai/developers/release-notes). - #### Output dimensions Use `ratio: { w: 16, h: 9 }`, `aspect_ratio: '16:9'`, or a pair of pixel dimensions (`width`, `height`). `ratio` takes precedence, followed by `width`/`height`, then `aspect_ratio`. `aspect_ratio` always denotes a shape. Legacy dimension pairs with both sides at most 32 also denote shapes; BytePlus retains its pixel-threshold rule described below. Invalid, non-positive, or non-finite dimensions fail with `bad_request`. @@ -118,9 +110,9 @@ Available when `provider: 'gemini'` or inferred from model: | Option | Type | Description | |--------|------|-------------| -| `model` | `String` | `'gemini-3.1-flash-image'` (default), `'gemini-3.1-flash-lite-image'`, or `'gemini-3-pro-image'`. | +| `model` | `String` | `'gemini-3.1-flash-image'` (default), `'gemini-nano-banana-2.1'` (Nano Banana 2.1), `'gemini-3.1-flash-lite-image'`, or `'gemini-3-pro-image'`. | | `ratio` | `Object` | Aspect ratio as `{ w, h }` (e.g., `{ w: 16, h: 9 }`). | -| `quality` | `String` | 3 Pro: `'1K'`, `'2K'`, `'4K'`; 3.1 Flash: `'512'`, `'1K'`, `'2K'`, `'4K'`; 3.1 Flash Lite: `'1K'`. Case-insensitive; a tier the model does not offer returns `bad_request` before any credits are checked. Defaults to the first supported tier: `'512'` on 3.1 Flash, `'1K'` on 3 Pro and 3.1 Flash Lite. | +| `quality` | `String` | Nano Banana 2.1 and 3 Pro: `'1K'`, `'2K'`, `'4K'`; 3.1 Flash: `'512'`, `'1K'`, `'2K'`, `'4K'`; 3.1 Flash Lite: `'1K'`. Case-insensitive; a tier the model does not offer returns `bad_request` before any credits are checked. Defaults to the first supported tier: `'512'` on 3.1 Flash, `'1K'` on Nano Banana 2.1, 3 Pro, and 3.1 Flash Lite. | | `input_images` | `Array` | Input images for image-to-image — a URL or base64/data-URI (URLs are fetched server-side). | #### xAI (Grok) Options