mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-13 23:29:50 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
f9cde23937
commit
b61448c8b7
4 files changed
+183
-14
No files matched your search
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<string, number> = {
|
||||
'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<string, number> = {
|
||||
'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<string, number> = {
|
||||
'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',
|
||||
|
||||
@@ -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<String>` | Input images for image-to-image — a URL or base64/data-URI (URLs are fetched server-side). |
|
||||
|
||||
#### xAI (Grok) Options
|
||||
|
||||
Reference in new issue
Block a user