Skip to content

Commit dac5a6f

Browse files
authored
✨ feat: rename Gemini 2.5 flash image to Nano Banana (#9004)
1 parent 44ffe14 commit dac5a6f

8 files changed

Lines changed: 118 additions & 38 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
// make stylelint work with tsx antd-style css template string
3535
"typescriptreact"
3636
],
37-
"vitest.maximumConfigs": 10,
37+
"vitest.maximumConfigs": 20,
3838
"workbench.editor.customLabels.patterns": {
3939
"**/app/**/[[]*[]]/[[]*[]]/page.tsx": "${dirname(2)}/${dirname(1)}/${dirname} • page component",
4040
"**/app/**/[[]*[]]/page.tsx": "${dirname(1)}/${dirname} • page component",
@@ -81,8 +81,7 @@
8181
"**/src/store/*/slices/*/reducer.ts": "${dirname(2)}/${dirname} • reducer",
8282

8383
"**/src/config/modelProviders/*.ts": "${filename} • provider",
84-
"**/src/config/aiModels/*.ts": "${filename} • model",
85-
"**/src/config/paramsSchemas/*/*.json": "${dirname(1)}/${filename} • params",
84+
"**/packages/model-bank/src/aiModels/aiModels/*.ts": "${filename} • model",
8685
"**/packages/model-runtime/src/*/index.ts": "${dirname} • runtime",
8786

8887
"**/src/server/services/*/index.ts": "${dirname} • server/service",

packages/database/src/models/__tests__/generationBatch.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { eq } from 'drizzle-orm';
33
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
44

5-
import { LobeChatDatabase } from '../../type';import { AsyncTaskStatus } from '@/types/asyncTask';
5+
import { AsyncTaskStatus } from '@/types/asyncTask';
66
import { GenerationConfig } from '@/types/generation';
77

88
import {
@@ -12,6 +12,7 @@ import {
1212
generations,
1313
users,
1414
} from '../../schemas';
15+
import { LobeChatDatabase } from '../../type';
1516
import { GenerationBatchModel } from '../generationBatch';
1617
import { getTestDB } from './_util';
1718

@@ -367,6 +368,51 @@ describe('GenerationBatchModel', () => {
367368
});
368369
});
369370

371+
it('should transform single config imageUrl through FileService', async () => {
372+
const [createdBatch] = await serverDB
373+
.insert(generationBatches)
374+
.values({
375+
...testBatch,
376+
userId,
377+
config: { imageUrl: 'single-image.jpg', prompt: 'test prompt' },
378+
})
379+
.returning();
380+
381+
const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
382+
testTopic.id,
383+
);
384+
385+
expect(results[0].config).toEqual({
386+
imageUrl: 'https://example.com/single-image.jpg',
387+
prompt: 'test prompt',
388+
});
389+
});
390+
391+
it('should transform both imageUrl and imageUrls when both are present', async () => {
392+
const [createdBatch] = await serverDB
393+
.insert(generationBatches)
394+
.values({
395+
...testBatch,
396+
userId,
397+
config: {
398+
imageUrl: 'single-image.jpg',
399+
imageUrls: ['url1.jpg', 'url2.jpg'],
400+
prompt: 'test prompt',
401+
},
402+
})
403+
.returning();
404+
405+
const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
406+
testTopic.id,
407+
);
408+
409+
expect(results[0].config).toEqual({
410+
imageUrl: 'https://example.com/single-image.jpg',
411+
imageUrls: ['https://example.com/url1.jpg', 'https://example.com/url2.jpg'],
412+
prompt: 'test prompt',
413+
});
414+
});
415+
370416
it('should handle config without imageUrls', async () => {
371417
const [createdBatch] = await serverDB
372418
.insert(generationBatches)

packages/database/src/models/generationBatch.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import debug from 'debug';
22
import { and, eq } from 'drizzle-orm';
33

4-
import { LobeChatDatabase } from '../type';
54
import { FileService } from '@/server/services/file';
65
import { Generation, GenerationAsset, GenerationBatch, GenerationConfig } from '@/types/generation';
76

@@ -11,6 +10,7 @@ import {
1110
NewGenerationBatch,
1211
generationBatches,
1312
} from '../schemas/generation';
13+
import { LobeChatDatabase } from '../type';
1414
import { GenerationModel } from './generation';
1515

1616
const log = debug('lobe-image:generation-batch-model');
@@ -121,6 +121,13 @@ export class GenerationBatchModel {
121121
// Transform config
122122
(async () => {
123123
const config = batch.config as GenerationConfig;
124+
125+
// Handle single imageUrl
126+
if (config.imageUrl) {
127+
config.imageUrl = await this.fileService.getFullFileUrl(config.imageUrl);
128+
}
129+
130+
// Handle imageUrls array
124131
if (Array.isArray(config.imageUrls)) {
125132
config.imageUrls = await Promise.all(
126133
config.imageUrls.map((url) => this.fileService.getFullFileUrl(url)),

packages/model-bank/src/aiModels/aihubmix.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ const aihubmixModels: AIChatModelCard[] = [
700700
},
701701
contextWindowTokens: 32_768 + 8192,
702702
description: 'Gemini 2.5 Flash 实验模型,支持图像生成',
703-
displayName: 'Gemini 2.5 Flash Image Preview',
703+
displayName: 'Nano Banana',
704704
id: 'gemini-2.5-flash-image-preview',
705705
maxOutput: 8192,
706706
pricing: {

packages/model-bank/src/aiModels/google.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ const googleChatModels: AIChatModelCard[] = [
196196
},
197197
contextWindowTokens: 32_768 + 8192,
198198
description:
199-
'Gemini 2.5 Flash Image Preview 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
200-
displayName: 'Gemini 2.5 Flash Image Preview',
199+
'Nano Banana 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
200+
displayName: 'Nano Banana',
201201
enabled: true,
202202
id: 'gemini-2.5-flash-image-preview',
203203
maxOutput: 8192,
@@ -610,12 +610,12 @@ const imagenBaseParameters: ModelParamsSchema = {
610610
/* eslint-disable sort-keys-fix/sort-keys-fix */
611611
const googleImageModels: AIImageModelCard[] = [
612612
{
613-
displayName: 'Gemini 2.5 Flash Image Preview',
613+
displayName: 'Nano Banana',
614614
id: 'gemini-2.5-flash-image-preview:image',
615615
enabled: true,
616616
type: 'image',
617617
description:
618-
'Gemini 2.5 Flash Image Preview 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
618+
'Nano Banana 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
619619
releasedAt: '2025-08-26',
620620
parameters: CHAT_MODEL_IMAGE_GENERATION_PARAMS,
621621
pricing: {

packages/model-bank/src/aiModels/openrouter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const openrouterChatModels: AIChatModelCard[] = [
3737
},
3838
contextWindowTokens: 32_768 + 8192,
3939
description: 'Gemini 2.5 Flash 实验模型,支持图像生成',
40-
displayName: 'Gemini 2.5 Flash Image Preview',
40+
displayName: 'Nano Banana',
4141
id: 'google/gemini-2.5-flash-image-preview',
4242
maxOutput: 8192,
4343
pricing: {
@@ -57,7 +57,7 @@ const openrouterChatModels: AIChatModelCard[] = [
5757
},
5858
contextWindowTokens: 32_768 + 8192,
5959
description: 'Gemini 2.5 Flash 实验模型,支持图像生成',
60-
displayName: 'Gemini 2.5 Flash Image Preview (free)',
60+
displayName: 'Nano Banana (free)',
6161
id: 'google/gemini-2.5-flash-image-preview:free',
6262
maxOutput: 8192,
6363
releasedAt: '2025-08-26',

packages/model-bank/src/aiModels/vertexai.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ const vertexaiChatModels: AIChatModelCard[] = [
126126
},
127127
contextWindowTokens: 32_768 + 8192,
128128
description:
129-
'Gemini 2.5 Flash Image Preview 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
130-
displayName: 'Gemini 2.5 Flash Image Preview',
129+
'Nano Banana 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
130+
displayName: 'Nano Banana',
131131
enabled: true,
132132
id: 'gemini-2.5-flash-image-preview',
133133
maxOutput: 8192,

packages/model-runtime/src/google/createImage.ts

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,40 @@ import { parseGoogleErrorMessage } from '../utils/googleErrorParser';
66
import { imageUrlToBase64 } from '../utils/imageToBase64';
77
import { parseDataUri } from '../utils/uriParser';
88

9+
// Maximum number of images allowed for processing
10+
const MAX_IMAGE_COUNT = 10;
11+
12+
/**
13+
* Process a single image URL and convert it to Google AI Part format
14+
*/
15+
async function processImageForParts(imageUrl: string): Promise<Part> {
16+
const { mimeType, base64, type } = parseDataUri(imageUrl);
17+
18+
if (type === 'base64') {
19+
if (!base64) {
20+
throw new TypeError("Image URL doesn't contain base64 data");
21+
}
22+
23+
return {
24+
inlineData: {
25+
data: base64,
26+
mimeType: mimeType || 'image/png',
27+
},
28+
};
29+
} else if (type === 'url') {
30+
const { base64: urlBase64, mimeType: urlMimeType } = await imageUrlToBase64(imageUrl);
31+
32+
return {
33+
inlineData: {
34+
data: urlBase64,
35+
mimeType: urlMimeType,
36+
},
37+
};
38+
} else {
39+
throw new TypeError(`currently we don't support image url: ${imageUrl}`);
40+
}
41+
}
42+
943
/**
1044
* Extract image data from generateContent response
1145
*/
@@ -71,36 +105,30 @@ async function generateImageByChatModel(
71105
const { model, params } = payload;
72106
const actualModel = model.replace(':image', '');
73107

108+
// Check for conflicting image parameters
109+
if (params.imageUrl && params.imageUrls && params.imageUrls.length > 0) {
110+
throw new TypeError('Cannot provide both imageUrl and imageUrls parameters simultaneously');
111+
}
112+
74113
// Build content parts
75114
const parts: Part[] = [{ text: params.prompt }];
76115

77116
// Add image for editing if provided
78117
if (params.imageUrl && params.imageUrl !== null) {
79-
const { mimeType, base64, type } = parseDataUri(params.imageUrl);
80-
81-
if (type === 'base64') {
82-
if (!base64) {
83-
throw new TypeError("Image URL doesn't contain base64 data");
84-
}
85-
86-
parts.push({
87-
inlineData: {
88-
data: base64,
89-
mimeType: mimeType || 'image/png',
90-
},
91-
});
92-
} else if (type === 'url') {
93-
const { base64: urlBase64, mimeType: urlMimeType } = await imageUrlToBase64(params.imageUrl);
94-
95-
parts.push({
96-
inlineData: {
97-
data: urlBase64,
98-
mimeType: urlMimeType,
99-
},
100-
});
101-
} else {
102-
throw new TypeError(`currently we don't support image url: ${params.imageUrl}`);
118+
const imagePart = await processImageForParts(params.imageUrl);
119+
parts.push(imagePart);
120+
}
121+
122+
// Add multiple images for editing if provided
123+
if (params.imageUrls && Array.isArray(params.imageUrls) && params.imageUrls.length > 0) {
124+
if (params.imageUrls.length > MAX_IMAGE_COUNT) {
125+
throw new TypeError(`Too many images provided. Maximum ${MAX_IMAGE_COUNT} images allowed`);
103126
}
127+
128+
const imageParts = await Promise.all(
129+
params.imageUrls.map((imageUrl) => processImageForParts(imageUrl)),
130+
);
131+
parts.push(...imageParts);
104132
}
105133

106134
const contents: Content[] = [

0 commit comments

Comments
 (0)