@lobehub/chat 1.116.3 → 1.117.0
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/.github/PULL_REQUEST_TEMPLATE.md +1 -0
- package/.github/workflows/release.yml +2 -0
- package/.i18nrc.js +1 -1
- package/CHANGELOG.md +117 -0
- package/changelog/v1.json +21 -0
- package/locales/ar/components.json +12 -0
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/components.json +12 -0
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/components.json +12 -0
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/components.json +12 -0
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/components.json +12 -0
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/components.json +12 -0
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/components.json +12 -0
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/components.json +12 -0
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/components.json +12 -0
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/components.json +12 -0
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/components.json +12 -0
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/components.json +12 -0
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/components.json +12 -0
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/components.json +12 -0
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/components.json +12 -0
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/components.json +12 -0
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/components.json +12 -0
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-TW/components.json +12 -0
- package/locales/zh-TW/models.json +3 -0
- package/package.json +5 -5
- package/packages/const/src/image.ts +9 -0
- package/packages/const/src/index.ts +2 -1
- package/packages/const/src/meta.ts +3 -2
- package/packages/const/src/settings/agent.ts +9 -4
- package/packages/const/src/settings/systemAgent.ts +0 -3
- package/packages/database/vitest.config.mts +1 -0
- package/packages/database/vitest.config.server.mts +1 -0
- package/packages/file-loaders/package.json +1 -1
- package/packages/file-loaders/vitest.config.mts +3 -7
- package/packages/model-runtime/src/RouterRuntime/createRuntime.ts +11 -9
- package/packages/model-runtime/src/google/createImage.test.ts +657 -0
- package/packages/model-runtime/src/google/createImage.ts +152 -0
- package/packages/model-runtime/src/google/index.test.ts +0 -328
- package/packages/model-runtime/src/google/index.ts +3 -40
- package/packages/model-runtime/src/utils/modelParse.ts +2 -1
- package/packages/model-runtime/src/utils/openaiCompatibleFactory/createImage.ts +239 -0
- package/packages/model-runtime/src/utils/openaiCompatibleFactory/index.test.ts +22 -22
- package/packages/model-runtime/src/utils/openaiCompatibleFactory/index.ts +9 -116
- package/packages/model-runtime/src/utils/postProcessModelList.ts +55 -0
- package/packages/model-runtime/src/utils/streams/google-ai.test.ts +7 -7
- package/packages/model-runtime/src/utils/streams/google-ai.ts +15 -2
- package/packages/model-runtime/src/utils/streams/openai/openai.test.ts +41 -0
- package/packages/model-runtime/src/utils/streams/openai/openai.ts +38 -2
- package/packages/model-runtime/src/utils/streams/protocol.test.ts +32 -0
- package/packages/model-runtime/src/utils/streams/protocol.ts +7 -3
- package/packages/model-runtime/src/utils/usageConverter.test.ts +58 -0
- package/packages/model-runtime/src/utils/usageConverter.ts +5 -1
- package/packages/model-runtime/vitest.config.mts +3 -0
- package/packages/prompts/package.json +0 -1
- package/packages/prompts/src/chains/__tests__/abstractChunk.test.ts +52 -0
- package/packages/prompts/src/chains/__tests__/answerWithContext.test.ts +100 -0
- package/packages/prompts/src/chains/__tests__/rewriteQuery.test.ts +88 -0
- package/packages/prompts/src/chains/__tests__/summaryGenerationTitle.test.ts +107 -0
- package/packages/prompts/src/chains/abstractChunk.ts +0 -2
- package/packages/prompts/src/chains/rewriteQuery.ts +3 -1
- package/packages/prompts/src/index.test.ts +41 -0
- package/packages/prompts/src/prompts/systemRole/index.test.ts +136 -0
- package/packages/prompts/vitest.config.mts +3 -0
- package/packages/types/src/index.ts +2 -0
- package/packages/utils/package.json +5 -1
- package/packages/utils/src/client/index.ts +2 -0
- package/packages/utils/src/server/index.ts +5 -0
- package/packages/utils/vitest.config.mts +4 -0
- package/src/app/(backend)/middleware/auth/index.test.ts +2 -2
- package/src/app/(backend)/middleware/auth/index.ts +1 -1
- package/src/app/(backend)/oidc/consent/route.ts +1 -2
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +2 -2
- package/src/app/(backend)/webapi/plugin/gateway/route.ts +1 -1
- package/src/app/[variants]/(main)/files/[id]/page.tsx +1 -1
- package/src/app/[variants]/(main)/settings/sync/page.tsx +1 -1
- package/src/app/[variants]/(main)/settings/system-agent/index.tsx +2 -1
- package/src/components/HtmlPreview/HtmlPreviewAction.tsx +32 -0
- package/src/components/HtmlPreview/PreviewDrawer.tsx +133 -0
- package/src/components/HtmlPreview/index.ts +2 -0
- package/src/config/aiModels/google.ts +42 -22
- package/src/config/aiModels/openrouter.ts +33 -0
- package/src/config/aiModels/vertexai.ts +4 -4
- package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +6 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +38 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +13 -1
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/index.tsx +1 -1
- package/src/features/Conversation/components/ChatItem/index.tsx +23 -0
- package/src/features/ShareModal/ShareJSON/index.tsx +2 -2
- package/src/features/ShareModal/ShareText/index.tsx +1 -1
- package/src/libs/oidc-provider/adapter.ts +1 -1
- package/src/libs/trpc/edge/middleware/jwtPayload.test.ts +1 -1
- package/src/libs/trpc/edge/middleware/jwtPayload.ts +1 -2
- package/src/libs/trpc/lambda/middleware/keyVaults.ts +1 -2
- package/src/locales/default/chat.ts +1 -0
- package/src/locales/default/components.ts +12 -0
- package/src/middleware.ts +3 -3
- package/src/server/routers/tools/search.test.ts +1 -1
- package/src/services/config.ts +2 -4
- package/src/utils/client/switchLang.ts +1 -1
- package/{packages/utils/src → src/utils}/server/pageProps.ts +2 -1
- package/tsconfig.json +1 -1
- package/vitest.config.mts +1 -0
- package/packages/model-runtime/src/UniformRuntime/index.ts +0 -117
- /package/{packages/const/src → src/const}/locale.ts +0 -0
- /package/{packages/utils/src → src/utils}/locale.test.ts +0 -0
- /package/{packages/utils/src → src/utils}/locale.ts +0 -0
- /package/{packages/utils/src → src/utils}/server/routeVariants.ts +0 -0
@@ -0,0 +1,657 @@
|
|
1
|
+
// @vitest-environment edge-runtime
|
2
|
+
import { GoogleGenAI } from '@google/genai';
|
3
|
+
import * as imageToBase64Module from '@lobechat/utils';
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
5
|
+
|
6
|
+
import { CreateImagePayload } from '@/libs/model-runtime/types/image';
|
7
|
+
|
8
|
+
import { createGoogleImage } from './createImage';
|
9
|
+
|
10
|
+
const provider = 'google';
|
11
|
+
const bizErrorType = 'ProviderBizError';
|
12
|
+
const invalidErrorType = 'InvalidProviderAPIKey';
|
13
|
+
|
14
|
+
// Mock the console.error to avoid polluting test output
|
15
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
16
|
+
|
17
|
+
let mockClient: GoogleGenAI;
|
18
|
+
|
19
|
+
beforeEach(() => {
|
20
|
+
mockClient = {
|
21
|
+
models: {
|
22
|
+
generateImages: vi.fn(),
|
23
|
+
generateContent: vi.fn(),
|
24
|
+
},
|
25
|
+
} as any;
|
26
|
+
});
|
27
|
+
|
28
|
+
describe('createGoogleImage', () => {
|
29
|
+
describe('Traditional Imagen Models', () => {
|
30
|
+
it('should create image successfully with basic parameters', async () => {
|
31
|
+
// Arrange - Use real base64 image data (5x5 red pixel PNG)
|
32
|
+
const realBase64ImageData =
|
33
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
|
34
|
+
const mockImageResponse = {
|
35
|
+
generatedImages: [
|
36
|
+
{
|
37
|
+
image: {
|
38
|
+
imageBytes: realBase64ImageData,
|
39
|
+
},
|
40
|
+
},
|
41
|
+
],
|
42
|
+
};
|
43
|
+
vi.spyOn(mockClient.models, 'generateImages').mockResolvedValue(mockImageResponse as any);
|
44
|
+
|
45
|
+
const payload: CreateImagePayload = {
|
46
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
47
|
+
params: {
|
48
|
+
prompt: 'A beautiful landscape with mountains and trees',
|
49
|
+
aspectRatio: '1:1',
|
50
|
+
},
|
51
|
+
};
|
52
|
+
|
53
|
+
// Act
|
54
|
+
const result = await createGoogleImage(mockClient, provider, payload);
|
55
|
+
|
56
|
+
// Assert
|
57
|
+
expect(mockClient.models.generateImages).toHaveBeenCalledWith({
|
58
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
59
|
+
prompt: 'A beautiful landscape with mountains and trees',
|
60
|
+
config: {
|
61
|
+
aspectRatio: '1:1',
|
62
|
+
numberOfImages: 1,
|
63
|
+
},
|
64
|
+
});
|
65
|
+
expect(result).toEqual({
|
66
|
+
imageUrl: `data:image/png;base64,${realBase64ImageData}`,
|
67
|
+
});
|
68
|
+
});
|
69
|
+
|
70
|
+
it('should support different aspect ratios like 16:9 for widescreen images', async () => {
|
71
|
+
// Arrange - Use real base64 data
|
72
|
+
const realBase64Data =
|
73
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
74
|
+
const mockImageResponse = {
|
75
|
+
generatedImages: [
|
76
|
+
{
|
77
|
+
image: {
|
78
|
+
imageBytes: realBase64Data,
|
79
|
+
},
|
80
|
+
},
|
81
|
+
],
|
82
|
+
};
|
83
|
+
vi.spyOn(mockClient.models, 'generateImages').mockResolvedValue(mockImageResponse as any);
|
84
|
+
|
85
|
+
const payload: CreateImagePayload = {
|
86
|
+
model: 'imagen-4.0-ultra-generate-preview-06-06',
|
87
|
+
params: {
|
88
|
+
prompt: 'Cinematic landscape shot with dramatic lighting',
|
89
|
+
aspectRatio: '16:9',
|
90
|
+
},
|
91
|
+
};
|
92
|
+
|
93
|
+
// Act
|
94
|
+
await createGoogleImage(mockClient, provider, payload);
|
95
|
+
|
96
|
+
// Assert
|
97
|
+
expect(mockClient.models.generateImages).toHaveBeenCalledWith({
|
98
|
+
model: 'imagen-4.0-ultra-generate-preview-06-06',
|
99
|
+
prompt: 'Cinematic landscape shot with dramatic lighting',
|
100
|
+
config: {
|
101
|
+
aspectRatio: '16:9',
|
102
|
+
numberOfImages: 1,
|
103
|
+
},
|
104
|
+
});
|
105
|
+
});
|
106
|
+
|
107
|
+
it('should work with only prompt when aspect ratio is not specified', async () => {
|
108
|
+
// Arrange
|
109
|
+
const realBase64Data =
|
110
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
111
|
+
const mockImageResponse = {
|
112
|
+
generatedImages: [
|
113
|
+
{
|
114
|
+
image: {
|
115
|
+
imageBytes: realBase64Data,
|
116
|
+
},
|
117
|
+
},
|
118
|
+
],
|
119
|
+
};
|
120
|
+
vi.spyOn(mockClient.models, 'generateImages').mockResolvedValue(mockImageResponse as any);
|
121
|
+
|
122
|
+
const payload: CreateImagePayload = {
|
123
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
124
|
+
params: {
|
125
|
+
prompt: 'A cute cat sitting in a garden',
|
126
|
+
},
|
127
|
+
};
|
128
|
+
|
129
|
+
// Act
|
130
|
+
await createGoogleImage(mockClient, provider, payload);
|
131
|
+
|
132
|
+
// Assert
|
133
|
+
expect(mockClient.models.generateImages).toHaveBeenCalledWith({
|
134
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
135
|
+
prompt: 'A cute cat sitting in a garden',
|
136
|
+
config: {
|
137
|
+
aspectRatio: undefined,
|
138
|
+
numberOfImages: 1,
|
139
|
+
},
|
140
|
+
});
|
141
|
+
});
|
142
|
+
|
143
|
+
describe('Error handling', () => {
|
144
|
+
it('should throw InvalidProviderAPIKey error when API key is invalid', async () => {
|
145
|
+
// Arrange - Use real Google AI error format
|
146
|
+
const message = `[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1/models/imagen-4.0:generateImages: [400 Bad Request] API key not valid. Please pass a valid API key. [{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"API_KEY_INVALID","domain":"googleapis.com","metadata":{"service":"generativelanguage.googleapis.com"}}]`;
|
147
|
+
const apiError = new Error(message);
|
148
|
+
vi.spyOn(mockClient.models, 'generateImages').mockRejectedValue(apiError);
|
149
|
+
|
150
|
+
const payload: CreateImagePayload = {
|
151
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
152
|
+
params: {
|
153
|
+
prompt: 'A realistic landscape photo',
|
154
|
+
},
|
155
|
+
};
|
156
|
+
|
157
|
+
// Act & Assert - Test error type rather than specific text
|
158
|
+
await expect(createGoogleImage(mockClient, provider, payload)).rejects.toEqual(
|
159
|
+
expect.objectContaining({
|
160
|
+
errorType: invalidErrorType,
|
161
|
+
provider,
|
162
|
+
}),
|
163
|
+
);
|
164
|
+
});
|
165
|
+
|
166
|
+
it('should throw ProviderBizError for network and API errors', async () => {
|
167
|
+
// Arrange
|
168
|
+
const apiError = new Error('Network connection failed');
|
169
|
+
vi.spyOn(mockClient.models, 'generateImages').mockRejectedValue(apiError);
|
170
|
+
|
171
|
+
const payload: CreateImagePayload = {
|
172
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
173
|
+
params: {
|
174
|
+
prompt: 'A digital art portrait',
|
175
|
+
},
|
176
|
+
};
|
177
|
+
|
178
|
+
// Act & Assert - Test error type and basic structure
|
179
|
+
await expect(createGoogleImage(mockClient, provider, payload)).rejects.toEqual(
|
180
|
+
expect.objectContaining({
|
181
|
+
errorType: bizErrorType,
|
182
|
+
provider,
|
183
|
+
error: expect.objectContaining({
|
184
|
+
message: expect.any(String),
|
185
|
+
}),
|
186
|
+
}),
|
187
|
+
);
|
188
|
+
});
|
189
|
+
|
190
|
+
it('should throw error when API response is malformed - missing generatedImages', async () => {
|
191
|
+
// Arrange
|
192
|
+
const mockImageResponse = {};
|
193
|
+
vi.spyOn(mockClient.models, 'generateImages').mockResolvedValue(mockImageResponse as any);
|
194
|
+
|
195
|
+
const payload: CreateImagePayload = {
|
196
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
197
|
+
params: {
|
198
|
+
prompt: 'Abstract geometric patterns',
|
199
|
+
},
|
200
|
+
};
|
201
|
+
|
202
|
+
// Act & Assert - Test error behavior rather than specific text
|
203
|
+
await expect(createGoogleImage(mockClient, provider, payload)).rejects.toEqual(
|
204
|
+
expect.objectContaining({
|
205
|
+
errorType: bizErrorType,
|
206
|
+
provider,
|
207
|
+
}),
|
208
|
+
);
|
209
|
+
});
|
210
|
+
|
211
|
+
it('should throw error when API response contains empty image array', async () => {
|
212
|
+
// Arrange
|
213
|
+
const mockImageResponse = {
|
214
|
+
generatedImages: [],
|
215
|
+
};
|
216
|
+
vi.spyOn(mockClient.models, 'generateImages').mockResolvedValue(mockImageResponse as any);
|
217
|
+
|
218
|
+
const payload: CreateImagePayload = {
|
219
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
220
|
+
params: {
|
221
|
+
prompt: 'Minimalist design poster',
|
222
|
+
},
|
223
|
+
};
|
224
|
+
|
225
|
+
// Act & Assert
|
226
|
+
await expect(createGoogleImage(mockClient, provider, payload)).rejects.toEqual(
|
227
|
+
expect.objectContaining({
|
228
|
+
errorType: bizErrorType,
|
229
|
+
provider,
|
230
|
+
}),
|
231
|
+
);
|
232
|
+
});
|
233
|
+
|
234
|
+
it('should throw error when generated image lacks required data', async () => {
|
235
|
+
// Arrange
|
236
|
+
const mockImageResponse = {
|
237
|
+
generatedImages: [
|
238
|
+
{
|
239
|
+
image: {}, // Missing imageBytes
|
240
|
+
},
|
241
|
+
],
|
242
|
+
};
|
243
|
+
vi.spyOn(mockClient.models, 'generateImages').mockResolvedValue(mockImageResponse as any);
|
244
|
+
|
245
|
+
const payload: CreateImagePayload = {
|
246
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
247
|
+
params: {
|
248
|
+
prompt: 'Watercolor painting style',
|
249
|
+
},
|
250
|
+
};
|
251
|
+
|
252
|
+
// Act & Assert
|
253
|
+
await expect(createGoogleImage(mockClient, provider, payload)).rejects.toEqual(
|
254
|
+
expect.objectContaining({
|
255
|
+
errorType: bizErrorType,
|
256
|
+
provider,
|
257
|
+
}),
|
258
|
+
);
|
259
|
+
});
|
260
|
+
});
|
261
|
+
|
262
|
+
describe('Edge cases', () => {
|
263
|
+
it('should return first image when API returns multiple generated images', async () => {
|
264
|
+
// Arrange - Use two different real base64 image data
|
265
|
+
const firstImageData =
|
266
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
|
267
|
+
const secondImageData =
|
268
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
269
|
+
const mockImageResponse = {
|
270
|
+
generatedImages: [
|
271
|
+
{
|
272
|
+
image: {
|
273
|
+
imageBytes: firstImageData,
|
274
|
+
},
|
275
|
+
},
|
276
|
+
{
|
277
|
+
image: {
|
278
|
+
imageBytes: secondImageData,
|
279
|
+
},
|
280
|
+
},
|
281
|
+
],
|
282
|
+
};
|
283
|
+
vi.spyOn(mockClient.models, 'generateImages').mockResolvedValue(mockImageResponse as any);
|
284
|
+
|
285
|
+
const payload: CreateImagePayload = {
|
286
|
+
model: 'imagen-4.0-generate-preview-06-06',
|
287
|
+
params: {
|
288
|
+
prompt: 'Generate multiple variations of a sunset',
|
289
|
+
},
|
290
|
+
};
|
291
|
+
|
292
|
+
// Act
|
293
|
+
const result = await createGoogleImage(mockClient, provider, payload);
|
294
|
+
|
295
|
+
// Assert - Should return the first image
|
296
|
+
expect(result).toEqual({
|
297
|
+
imageUrl: `data:image/png;base64,${firstImageData}`,
|
298
|
+
});
|
299
|
+
});
|
300
|
+
|
301
|
+
it('should work with custom future Imagen model versions', async () => {
|
302
|
+
// Arrange
|
303
|
+
const realBase64Data =
|
304
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
305
|
+
const mockImageResponse = {
|
306
|
+
generatedImages: [
|
307
|
+
{
|
308
|
+
image: {
|
309
|
+
imageBytes: realBase64Data,
|
310
|
+
},
|
311
|
+
},
|
312
|
+
],
|
313
|
+
};
|
314
|
+
vi.spyOn(mockClient.models, 'generateImages').mockResolvedValue(mockImageResponse as any);
|
315
|
+
|
316
|
+
const payload: CreateImagePayload = {
|
317
|
+
model: 'imagen-5.0-future-model',
|
318
|
+
params: {
|
319
|
+
prompt: 'Photorealistic portrait with soft lighting',
|
320
|
+
aspectRatio: '4:3',
|
321
|
+
},
|
322
|
+
};
|
323
|
+
|
324
|
+
// Act
|
325
|
+
await createGoogleImage(mockClient, provider, payload);
|
326
|
+
|
327
|
+
// Assert
|
328
|
+
expect(mockClient.models.generateImages).toHaveBeenCalledWith({
|
329
|
+
model: 'imagen-5.0-future-model',
|
330
|
+
prompt: 'Photorealistic portrait with soft lighting',
|
331
|
+
config: {
|
332
|
+
aspectRatio: '4:3',
|
333
|
+
numberOfImages: 1,
|
334
|
+
},
|
335
|
+
});
|
336
|
+
});
|
337
|
+
});
|
338
|
+
});
|
339
|
+
|
340
|
+
describe('Gemini 2.5 Flash Image Models (:image)', () => {
|
341
|
+
it('should create image successfully using generateContent for :image model', async () => {
|
342
|
+
// Arrange
|
343
|
+
const realBase64ImageData =
|
344
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
|
345
|
+
const mockContentResponse = {
|
346
|
+
candidates: [
|
347
|
+
{
|
348
|
+
content: {
|
349
|
+
parts: [
|
350
|
+
{
|
351
|
+
inlineData: {
|
352
|
+
data: realBase64ImageData,
|
353
|
+
mimeType: 'image/png',
|
354
|
+
},
|
355
|
+
},
|
356
|
+
],
|
357
|
+
},
|
358
|
+
},
|
359
|
+
],
|
360
|
+
};
|
361
|
+
vi.spyOn(mockClient.models, 'generateContent').mockResolvedValue(mockContentResponse as any);
|
362
|
+
|
363
|
+
const payload: CreateImagePayload = {
|
364
|
+
model: 'gemini-2.5-flash-image-preview:image',
|
365
|
+
params: {
|
366
|
+
prompt: 'Create a beautiful sunset landscape',
|
367
|
+
},
|
368
|
+
};
|
369
|
+
|
370
|
+
// Act
|
371
|
+
const result = await createGoogleImage(mockClient, provider, payload);
|
372
|
+
|
373
|
+
// Assert
|
374
|
+
expect(mockClient.models.generateContent).toHaveBeenCalledWith({
|
375
|
+
contents: [
|
376
|
+
{
|
377
|
+
role: 'user',
|
378
|
+
parts: [{ text: 'Create a beautiful sunset landscape' }],
|
379
|
+
},
|
380
|
+
],
|
381
|
+
model: 'gemini-2.5-flash-image-preview',
|
382
|
+
config: {
|
383
|
+
responseModalities: ['Image'],
|
384
|
+
},
|
385
|
+
});
|
386
|
+
expect(result).toEqual({
|
387
|
+
imageUrl: `data:image/png;base64,${realBase64ImageData}`,
|
388
|
+
});
|
389
|
+
});
|
390
|
+
|
391
|
+
it('should support image editing with base64 imageUrl', async () => {
|
392
|
+
// Arrange
|
393
|
+
const inputImageBase64 =
|
394
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
395
|
+
const outputImageBase64 =
|
396
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
|
397
|
+
|
398
|
+
const mockContentResponse = {
|
399
|
+
candidates: [
|
400
|
+
{
|
401
|
+
content: {
|
402
|
+
parts: [
|
403
|
+
{
|
404
|
+
inlineData: {
|
405
|
+
data: outputImageBase64,
|
406
|
+
mimeType: 'image/png',
|
407
|
+
},
|
408
|
+
},
|
409
|
+
],
|
410
|
+
},
|
411
|
+
},
|
412
|
+
],
|
413
|
+
};
|
414
|
+
vi.spyOn(mockClient.models, 'generateContent').mockResolvedValue(mockContentResponse as any);
|
415
|
+
|
416
|
+
const payload: CreateImagePayload = {
|
417
|
+
model: 'gemini-2.5-flash-image-preview:image',
|
418
|
+
params: {
|
419
|
+
prompt: 'Add a red rose to this image',
|
420
|
+
imageUrl: `data:image/png;base64,${inputImageBase64}`,
|
421
|
+
},
|
422
|
+
};
|
423
|
+
|
424
|
+
// Act
|
425
|
+
const result = await createGoogleImage(mockClient, provider, payload);
|
426
|
+
|
427
|
+
// Assert
|
428
|
+
expect(mockClient.models.generateContent).toHaveBeenCalledWith({
|
429
|
+
contents: [
|
430
|
+
{
|
431
|
+
role: 'user',
|
432
|
+
parts: [
|
433
|
+
{ text: 'Add a red rose to this image' },
|
434
|
+
{
|
435
|
+
inlineData: {
|
436
|
+
data: inputImageBase64,
|
437
|
+
mimeType: 'image/png',
|
438
|
+
},
|
439
|
+
},
|
440
|
+
],
|
441
|
+
},
|
442
|
+
],
|
443
|
+
model: 'gemini-2.5-flash-image-preview',
|
444
|
+
config: {
|
445
|
+
responseModalities: ['Image'],
|
446
|
+
},
|
447
|
+
});
|
448
|
+
expect(result).toEqual({
|
449
|
+
imageUrl: `data:image/png;base64,${outputImageBase64}`,
|
450
|
+
});
|
451
|
+
});
|
452
|
+
|
453
|
+
it('should support image editing with URL imageUrl', async () => {
|
454
|
+
// Arrange
|
455
|
+
const inputImageBase64 =
|
456
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
457
|
+
const outputImageBase64 =
|
458
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
|
459
|
+
|
460
|
+
// Mock imageUrlToBase64 utility
|
461
|
+
vi.spyOn(imageToBase64Module, 'imageUrlToBase64').mockResolvedValue({
|
462
|
+
base64: inputImageBase64,
|
463
|
+
mimeType: 'image/jpeg',
|
464
|
+
});
|
465
|
+
|
466
|
+
const mockContentResponse = {
|
467
|
+
candidates: [
|
468
|
+
{
|
469
|
+
content: {
|
470
|
+
parts: [
|
471
|
+
{
|
472
|
+
inlineData: {
|
473
|
+
data: outputImageBase64,
|
474
|
+
mimeType: 'image/png',
|
475
|
+
},
|
476
|
+
},
|
477
|
+
],
|
478
|
+
},
|
479
|
+
},
|
480
|
+
],
|
481
|
+
};
|
482
|
+
vi.spyOn(mockClient.models, 'generateContent').mockResolvedValue(mockContentResponse as any);
|
483
|
+
|
484
|
+
const payload: CreateImagePayload = {
|
485
|
+
model: 'gemini-2.5-flash-image-preview:image',
|
486
|
+
params: {
|
487
|
+
prompt: 'Change the background to blue sky',
|
488
|
+
imageUrl: 'https://example.com/image.jpg',
|
489
|
+
},
|
490
|
+
};
|
491
|
+
|
492
|
+
// Act
|
493
|
+
const result = await createGoogleImage(mockClient, provider, payload);
|
494
|
+
|
495
|
+
// Assert
|
496
|
+
expect(imageToBase64Module.imageUrlToBase64).toHaveBeenCalledWith(
|
497
|
+
'https://example.com/image.jpg',
|
498
|
+
);
|
499
|
+
expect(mockClient.models.generateContent).toHaveBeenCalledWith({
|
500
|
+
contents: [
|
501
|
+
{
|
502
|
+
role: 'user',
|
503
|
+
parts: [
|
504
|
+
{ text: 'Change the background to blue sky' },
|
505
|
+
{
|
506
|
+
inlineData: {
|
507
|
+
data: inputImageBase64,
|
508
|
+
mimeType: 'image/jpeg',
|
509
|
+
},
|
510
|
+
},
|
511
|
+
],
|
512
|
+
},
|
513
|
+
],
|
514
|
+
model: 'gemini-2.5-flash-image-preview',
|
515
|
+
config: {
|
516
|
+
responseModalities: ['Image'],
|
517
|
+
},
|
518
|
+
});
|
519
|
+
expect(result).toEqual({
|
520
|
+
imageUrl: `data:image/png;base64,${outputImageBase64}`,
|
521
|
+
});
|
522
|
+
});
|
523
|
+
|
524
|
+
it('should handle null imageUrl as text-only generation', async () => {
|
525
|
+
// Arrange
|
526
|
+
const outputImageBase64 =
|
527
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
|
528
|
+
|
529
|
+
const mockContentResponse = {
|
530
|
+
candidates: [
|
531
|
+
{
|
532
|
+
content: {
|
533
|
+
parts: [
|
534
|
+
{
|
535
|
+
inlineData: {
|
536
|
+
data: outputImageBase64,
|
537
|
+
mimeType: 'image/png',
|
538
|
+
},
|
539
|
+
},
|
540
|
+
],
|
541
|
+
},
|
542
|
+
},
|
543
|
+
],
|
544
|
+
};
|
545
|
+
vi.spyOn(mockClient.models, 'generateContent').mockResolvedValue(mockContentResponse as any);
|
546
|
+
|
547
|
+
const payload: CreateImagePayload = {
|
548
|
+
model: 'gemini-2.5-flash-image-preview:image',
|
549
|
+
params: {
|
550
|
+
prompt: 'Generate a colorful abstract pattern',
|
551
|
+
imageUrl: null,
|
552
|
+
},
|
553
|
+
};
|
554
|
+
|
555
|
+
// Act
|
556
|
+
const result = await createGoogleImage(mockClient, provider, payload);
|
557
|
+
|
558
|
+
// Assert
|
559
|
+
expect(mockClient.models.generateContent).toHaveBeenCalledWith({
|
560
|
+
contents: [
|
561
|
+
{
|
562
|
+
role: 'user',
|
563
|
+
parts: [{ text: 'Generate a colorful abstract pattern' }],
|
564
|
+
},
|
565
|
+
],
|
566
|
+
model: 'gemini-2.5-flash-image-preview',
|
567
|
+
config: {
|
568
|
+
responseModalities: ['Image'],
|
569
|
+
},
|
570
|
+
});
|
571
|
+
expect(result).toEqual({
|
572
|
+
imageUrl: `data:image/png;base64,${outputImageBase64}`,
|
573
|
+
});
|
574
|
+
});
|
575
|
+
|
576
|
+
describe('Error handling for :image models', () => {
|
577
|
+
it('should throw error when no image generated in response', async () => {
|
578
|
+
// Arrange
|
579
|
+
const mockContentResponse = {
|
580
|
+
candidates: [
|
581
|
+
{
|
582
|
+
content: {
|
583
|
+
parts: [
|
584
|
+
{
|
585
|
+
text: 'I cannot generate an image.',
|
586
|
+
},
|
587
|
+
],
|
588
|
+
},
|
589
|
+
},
|
590
|
+
],
|
591
|
+
};
|
592
|
+
vi.spyOn(mockClient.models, 'generateContent').mockResolvedValue(
|
593
|
+
mockContentResponse as any,
|
594
|
+
);
|
595
|
+
|
596
|
+
const payload: CreateImagePayload = {
|
597
|
+
model: 'gemini-2.5-flash-image-preview:image',
|
598
|
+
params: {
|
599
|
+
prompt: 'Create inappropriate content',
|
600
|
+
},
|
601
|
+
};
|
602
|
+
|
603
|
+
// Act & Assert
|
604
|
+
await expect(createGoogleImage(mockClient, provider, payload)).rejects.toEqual(
|
605
|
+
expect.objectContaining({
|
606
|
+
errorType: bizErrorType,
|
607
|
+
provider,
|
608
|
+
}),
|
609
|
+
);
|
610
|
+
});
|
611
|
+
|
612
|
+
it('should throw error when response is malformed', async () => {
|
613
|
+
// Arrange
|
614
|
+
const mockContentResponse = {
|
615
|
+
candidates: [],
|
616
|
+
};
|
617
|
+
vi.spyOn(mockClient.models, 'generateContent').mockResolvedValue(
|
618
|
+
mockContentResponse as any,
|
619
|
+
);
|
620
|
+
|
621
|
+
const payload: CreateImagePayload = {
|
622
|
+
model: 'gemini-2.5-flash-image-preview:image',
|
623
|
+
params: {
|
624
|
+
prompt: 'Generate an image',
|
625
|
+
},
|
626
|
+
};
|
627
|
+
|
628
|
+
// Act & Assert
|
629
|
+
await expect(createGoogleImage(mockClient, provider, payload)).rejects.toEqual(
|
630
|
+
expect.objectContaining({
|
631
|
+
errorType: bizErrorType,
|
632
|
+
provider,
|
633
|
+
}),
|
634
|
+
);
|
635
|
+
});
|
636
|
+
|
637
|
+
it('should throw error for unsupported image URL format', async () => {
|
638
|
+
// Arrange
|
639
|
+
const payload: CreateImagePayload = {
|
640
|
+
model: 'gemini-2.5-flash-image-preview:image',
|
641
|
+
params: {
|
642
|
+
prompt: 'Edit this image',
|
643
|
+
imageUrl: 'ftp://example.com/image.jpg',
|
644
|
+
},
|
645
|
+
};
|
646
|
+
|
647
|
+
// Act & Assert
|
648
|
+
await expect(createGoogleImage(mockClient, provider, payload)).rejects.toEqual(
|
649
|
+
expect.objectContaining({
|
650
|
+
errorType: bizErrorType,
|
651
|
+
provider,
|
652
|
+
}),
|
653
|
+
);
|
654
|
+
});
|
655
|
+
});
|
656
|
+
});
|
657
|
+
});
|