@ai-sdk/gateway 0.0.0-70e0935a-20260114150030 → 0.0.0-98261322-20260122142521
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/CHANGELOG.md +49 -4
- package/dist/index.d.mts +20 -10
- package/dist/index.d.ts +20 -10
- package/dist/index.js +62 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -25
- package/dist/index.mjs.map +1 -1
- package/docs/00-ai-gateway.mdx +625 -0
- package/package.json +12 -5
- package/src/errors/as-gateway-error.ts +33 -0
- package/src/errors/create-gateway-error.test.ts +590 -0
- package/src/errors/create-gateway-error.ts +132 -0
- package/src/errors/extract-api-call-response.test.ts +270 -0
- package/src/errors/extract-api-call-response.ts +15 -0
- package/src/errors/gateway-authentication-error.ts +84 -0
- package/src/errors/gateway-error-types.test.ts +278 -0
- package/src/errors/gateway-error.ts +47 -0
- package/src/errors/gateway-internal-server-error.ts +33 -0
- package/src/errors/gateway-invalid-request-error.ts +33 -0
- package/src/errors/gateway-model-not-found-error.ts +47 -0
- package/src/errors/gateway-rate-limit-error.ts +33 -0
- package/src/errors/gateway-response-error.ts +42 -0
- package/src/errors/index.ts +16 -0
- package/src/errors/parse-auth-method.test.ts +136 -0
- package/src/errors/parse-auth-method.ts +23 -0
- package/src/gateway-config.ts +7 -0
- package/src/gateway-embedding-model-settings.ts +22 -0
- package/src/gateway-embedding-model.test.ts +213 -0
- package/src/gateway-embedding-model.ts +109 -0
- package/src/gateway-fetch-metadata.test.ts +774 -0
- package/src/gateway-fetch-metadata.ts +127 -0
- package/src/gateway-image-model-settings.ts +12 -0
- package/src/gateway-image-model.test.ts +823 -0
- package/src/gateway-image-model.ts +145 -0
- package/src/gateway-language-model-settings.ts +159 -0
- package/src/gateway-language-model.test.ts +1485 -0
- package/src/gateway-language-model.ts +212 -0
- package/src/gateway-model-entry.ts +58 -0
- package/src/gateway-provider-options.ts +66 -0
- package/src/gateway-provider.test.ts +1210 -0
- package/src/gateway-provider.ts +284 -0
- package/src/gateway-tools.ts +15 -0
- package/src/index.ts +27 -0
- package/src/tool/perplexity-search.ts +294 -0
- package/src/vercel-environment.test.ts +65 -0
- package/src/vercel-environment.ts +6 -0
- package/src/version.ts +6 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { GatewayImageModel } from './gateway-image-model';
|
|
3
|
+
import { createTestServer } from '@ai-sdk/test-server/with-vitest';
|
|
4
|
+
import type { GatewayConfig } from './gateway-config';
|
|
5
|
+
|
|
6
|
+
const TEST_MODEL_ID = 'google/imagen-4.0-generate';
|
|
7
|
+
|
|
8
|
+
const createTestModel = (
|
|
9
|
+
config: Partial<
|
|
10
|
+
GatewayConfig & { o11yHeaders?: Record<string, string> }
|
|
11
|
+
> = {},
|
|
12
|
+
) => {
|
|
13
|
+
return new GatewayImageModel(TEST_MODEL_ID, {
|
|
14
|
+
provider: 'gateway',
|
|
15
|
+
baseURL: 'https://api.test.com',
|
|
16
|
+
headers: () => ({
|
|
17
|
+
Authorization: 'Bearer test-token',
|
|
18
|
+
'ai-gateway-auth-method': 'api-key',
|
|
19
|
+
}),
|
|
20
|
+
fetch: globalThis.fetch,
|
|
21
|
+
o11yHeaders: config.o11yHeaders || {},
|
|
22
|
+
...config,
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('GatewayImageModel', () => {
|
|
27
|
+
const server = createTestServer({
|
|
28
|
+
'https://api.test.com/image-model': {},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('constructor', () => {
|
|
32
|
+
it('should create instance with correct properties', () => {
|
|
33
|
+
const model = createTestModel();
|
|
34
|
+
|
|
35
|
+
expect(model.modelId).toBe(TEST_MODEL_ID);
|
|
36
|
+
expect(model.provider).toBe('gateway');
|
|
37
|
+
expect(model.specificationVersion).toBe('v3');
|
|
38
|
+
expect(model.maxImagesPerCall).toBe(Number.MAX_SAFE_INTEGER);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should avoid client-side splitting even for BFL models', () => {
|
|
42
|
+
const model = new GatewayImageModel('bfl/flux-pro-1.1', {
|
|
43
|
+
provider: 'gateway',
|
|
44
|
+
baseURL: 'https://api.test.com',
|
|
45
|
+
headers: async () => ({}),
|
|
46
|
+
fetch: globalThis.fetch,
|
|
47
|
+
o11yHeaders: {},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(model.maxImagesPerCall).toBe(Number.MAX_SAFE_INTEGER);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should accept custom provider name', () => {
|
|
54
|
+
const model = new GatewayImageModel(TEST_MODEL_ID, {
|
|
55
|
+
provider: 'custom-gateway',
|
|
56
|
+
baseURL: 'https://api.test.com',
|
|
57
|
+
headers: async () => ({}),
|
|
58
|
+
fetch: globalThis.fetch,
|
|
59
|
+
o11yHeaders: {},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(model.provider).toBe('custom-gateway');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('doGenerate', () => {
|
|
67
|
+
function prepareJsonResponse({
|
|
68
|
+
images = ['base64-image-1'],
|
|
69
|
+
warnings,
|
|
70
|
+
providerMetadata,
|
|
71
|
+
}: {
|
|
72
|
+
images?: string[];
|
|
73
|
+
warnings?: Array<{ type: 'other'; message: string }>;
|
|
74
|
+
providerMetadata?: Record<string, unknown>;
|
|
75
|
+
} = {}) {
|
|
76
|
+
server.urls['https://api.test.com/image-model'].response = {
|
|
77
|
+
type: 'json-value',
|
|
78
|
+
body: {
|
|
79
|
+
images,
|
|
80
|
+
...(warnings && { warnings }),
|
|
81
|
+
...(providerMetadata && { providerMetadata }),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
it('should send correct request headers', async () => {
|
|
87
|
+
prepareJsonResponse();
|
|
88
|
+
|
|
89
|
+
await createTestModel().doGenerate({
|
|
90
|
+
prompt: 'A beautiful sunset over mountains',
|
|
91
|
+
files: undefined,
|
|
92
|
+
mask: undefined,
|
|
93
|
+
n: 1,
|
|
94
|
+
size: undefined,
|
|
95
|
+
aspectRatio: undefined,
|
|
96
|
+
seed: undefined,
|
|
97
|
+
providerOptions: {},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const headers = server.calls[0].requestHeaders;
|
|
101
|
+
expect(headers).toMatchObject({
|
|
102
|
+
authorization: 'Bearer test-token',
|
|
103
|
+
'ai-image-model-specification-version': '3',
|
|
104
|
+
'ai-model-id': TEST_MODEL_ID,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should send correct request body with all parameters', async () => {
|
|
109
|
+
prepareJsonResponse({ images: ['base64-1', 'base64-2'] });
|
|
110
|
+
|
|
111
|
+
const prompt = 'A cat playing piano';
|
|
112
|
+
await createTestModel().doGenerate({
|
|
113
|
+
prompt,
|
|
114
|
+
files: undefined,
|
|
115
|
+
mask: undefined,
|
|
116
|
+
n: 2,
|
|
117
|
+
size: '1024x1024',
|
|
118
|
+
aspectRatio: '16:9',
|
|
119
|
+
seed: 42,
|
|
120
|
+
providerOptions: {
|
|
121
|
+
vertex: { safetySettings: 'block_none' },
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
126
|
+
expect(requestBody).toEqual({
|
|
127
|
+
prompt,
|
|
128
|
+
n: 2,
|
|
129
|
+
size: '1024x1024',
|
|
130
|
+
aspectRatio: '16:9',
|
|
131
|
+
seed: 42,
|
|
132
|
+
providerOptions: { vertex: { safetySettings: 'block_none' } },
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should omit optional parameters when not provided', async () => {
|
|
137
|
+
prepareJsonResponse();
|
|
138
|
+
|
|
139
|
+
const prompt = 'A simple prompt';
|
|
140
|
+
await createTestModel().doGenerate({
|
|
141
|
+
prompt,
|
|
142
|
+
files: undefined,
|
|
143
|
+
mask: undefined,
|
|
144
|
+
n: 1,
|
|
145
|
+
size: undefined,
|
|
146
|
+
aspectRatio: undefined,
|
|
147
|
+
seed: undefined,
|
|
148
|
+
providerOptions: {},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
152
|
+
expect(requestBody).toEqual({
|
|
153
|
+
prompt,
|
|
154
|
+
n: 1,
|
|
155
|
+
providerOptions: {},
|
|
156
|
+
});
|
|
157
|
+
expect(requestBody).not.toHaveProperty('size');
|
|
158
|
+
expect(requestBody).not.toHaveProperty('aspectRatio');
|
|
159
|
+
expect(requestBody).not.toHaveProperty('seed');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return images array correctly', async () => {
|
|
163
|
+
const mockImages = ['base64-image-1', 'base64-image-2'];
|
|
164
|
+
prepareJsonResponse({ images: mockImages });
|
|
165
|
+
|
|
166
|
+
const result = await createTestModel().doGenerate({
|
|
167
|
+
prompt: 'Test prompt',
|
|
168
|
+
files: undefined,
|
|
169
|
+
mask: undefined,
|
|
170
|
+
n: 2,
|
|
171
|
+
size: undefined,
|
|
172
|
+
aspectRatio: undefined,
|
|
173
|
+
seed: undefined,
|
|
174
|
+
providerOptions: {},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(result.images).toEqual(mockImages);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return provider metadata correctly', async () => {
|
|
181
|
+
const mockProviderMetadata = {
|
|
182
|
+
vertex: {
|
|
183
|
+
images: [
|
|
184
|
+
{ revisedPrompt: 'Revised prompt 1' },
|
|
185
|
+
{ revisedPrompt: 'Revised prompt 2' },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
gateway: {
|
|
189
|
+
routing: { provider: 'vertex' },
|
|
190
|
+
cost: '0.08',
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
prepareJsonResponse({
|
|
195
|
+
images: ['base64-1', 'base64-2'],
|
|
196
|
+
providerMetadata: mockProviderMetadata,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const result = await createTestModel().doGenerate({
|
|
200
|
+
prompt: 'Test prompt',
|
|
201
|
+
files: undefined,
|
|
202
|
+
mask: undefined,
|
|
203
|
+
n: 2,
|
|
204
|
+
size: undefined,
|
|
205
|
+
aspectRatio: undefined,
|
|
206
|
+
seed: undefined,
|
|
207
|
+
providerOptions: {},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(result.providerMetadata).toEqual(mockProviderMetadata);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle provider metadata without images field', async () => {
|
|
214
|
+
prepareJsonResponse({
|
|
215
|
+
images: ['base64-1'],
|
|
216
|
+
providerMetadata: {
|
|
217
|
+
gateway: {
|
|
218
|
+
routing: { provider: 'vertex' },
|
|
219
|
+
cost: '0.04',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await createTestModel().doGenerate({
|
|
225
|
+
prompt: 'Test prompt',
|
|
226
|
+
files: undefined,
|
|
227
|
+
mask: undefined,
|
|
228
|
+
n: 1,
|
|
229
|
+
size: undefined,
|
|
230
|
+
aspectRatio: undefined,
|
|
231
|
+
seed: undefined,
|
|
232
|
+
providerOptions: {},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(result.providerMetadata).toEqual({
|
|
236
|
+
gateway: {
|
|
237
|
+
routing: { provider: 'vertex' },
|
|
238
|
+
cost: '0.04',
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should handle empty provider metadata', async () => {
|
|
244
|
+
prepareJsonResponse({
|
|
245
|
+
images: ['base64-1'],
|
|
246
|
+
providerMetadata: {},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const result = await createTestModel().doGenerate({
|
|
250
|
+
prompt: 'Test prompt',
|
|
251
|
+
files: undefined,
|
|
252
|
+
mask: undefined,
|
|
253
|
+
n: 1,
|
|
254
|
+
size: undefined,
|
|
255
|
+
aspectRatio: undefined,
|
|
256
|
+
seed: undefined,
|
|
257
|
+
providerOptions: {},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(result.providerMetadata).toEqual({});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should handle undefined provider metadata', async () => {
|
|
264
|
+
prepareJsonResponse({
|
|
265
|
+
images: ['base64-1'],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = await createTestModel().doGenerate({
|
|
269
|
+
prompt: 'Test prompt',
|
|
270
|
+
files: undefined,
|
|
271
|
+
mask: undefined,
|
|
272
|
+
n: 1,
|
|
273
|
+
size: undefined,
|
|
274
|
+
aspectRatio: undefined,
|
|
275
|
+
seed: undefined,
|
|
276
|
+
providerOptions: {},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(result.providerMetadata).toBeUndefined();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should return warnings when provided', async () => {
|
|
283
|
+
const mockWarnings = [
|
|
284
|
+
{ type: 'other' as const, message: 'Setting not supported' },
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
prepareJsonResponse({
|
|
288
|
+
images: ['base64-1'],
|
|
289
|
+
warnings: mockWarnings,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const result = await createTestModel().doGenerate({
|
|
293
|
+
prompt: 'Test prompt',
|
|
294
|
+
files: undefined,
|
|
295
|
+
mask: undefined,
|
|
296
|
+
n: 1,
|
|
297
|
+
size: undefined,
|
|
298
|
+
aspectRatio: undefined,
|
|
299
|
+
seed: undefined,
|
|
300
|
+
providerOptions: {},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(result.warnings).toEqual(mockWarnings);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should return empty warnings array when not provided', async () => {
|
|
307
|
+
prepareJsonResponse({
|
|
308
|
+
images: ['base64-1'],
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const result = await createTestModel().doGenerate({
|
|
312
|
+
prompt: 'Test prompt',
|
|
313
|
+
files: undefined,
|
|
314
|
+
mask: undefined,
|
|
315
|
+
n: 1,
|
|
316
|
+
size: undefined,
|
|
317
|
+
aspectRatio: undefined,
|
|
318
|
+
seed: undefined,
|
|
319
|
+
providerOptions: {},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(result.warnings).toEqual([]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should include response metadata', async () => {
|
|
326
|
+
prepareJsonResponse({
|
|
327
|
+
images: ['base64-1'],
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const result = await createTestModel().doGenerate({
|
|
331
|
+
prompt: 'Test prompt',
|
|
332
|
+
files: undefined,
|
|
333
|
+
mask: undefined,
|
|
334
|
+
n: 1,
|
|
335
|
+
size: undefined,
|
|
336
|
+
aspectRatio: undefined,
|
|
337
|
+
seed: undefined,
|
|
338
|
+
providerOptions: {},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(result.response.modelId).toBe(TEST_MODEL_ID);
|
|
342
|
+
expect(result.response.timestamp).toBeInstanceOf(Date);
|
|
343
|
+
expect(result.response.headers).toBeDefined();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should merge custom headers with config headers', async () => {
|
|
347
|
+
prepareJsonResponse();
|
|
348
|
+
|
|
349
|
+
await createTestModel().doGenerate({
|
|
350
|
+
prompt: 'Test prompt',
|
|
351
|
+
files: undefined,
|
|
352
|
+
mask: undefined,
|
|
353
|
+
n: 1,
|
|
354
|
+
size: undefined,
|
|
355
|
+
aspectRatio: undefined,
|
|
356
|
+
seed: undefined,
|
|
357
|
+
headers: {
|
|
358
|
+
'X-Custom-Header': 'custom-value',
|
|
359
|
+
},
|
|
360
|
+
providerOptions: {},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const headers = server.calls[0].requestHeaders;
|
|
364
|
+
expect(headers).toMatchObject({
|
|
365
|
+
authorization: 'Bearer test-token',
|
|
366
|
+
'x-custom-header': 'custom-value',
|
|
367
|
+
'ai-image-model-specification-version': '3',
|
|
368
|
+
'ai-model-id': TEST_MODEL_ID,
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should include o11y headers', async () => {
|
|
373
|
+
prepareJsonResponse();
|
|
374
|
+
|
|
375
|
+
await createTestModel({
|
|
376
|
+
o11yHeaders: {
|
|
377
|
+
'ai-o11y-deployment-id': 'dpl_123',
|
|
378
|
+
'ai-o11y-environment': 'production',
|
|
379
|
+
},
|
|
380
|
+
}).doGenerate({
|
|
381
|
+
prompt: 'Test prompt',
|
|
382
|
+
files: undefined,
|
|
383
|
+
mask: undefined,
|
|
384
|
+
n: 1,
|
|
385
|
+
size: undefined,
|
|
386
|
+
aspectRatio: undefined,
|
|
387
|
+
seed: undefined,
|
|
388
|
+
providerOptions: {},
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const headers = server.calls[0].requestHeaders;
|
|
392
|
+
expect(headers).toMatchObject({
|
|
393
|
+
'ai-o11y-deployment-id': 'dpl_123',
|
|
394
|
+
'ai-o11y-environment': 'production',
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should pass abort signal to fetch', async () => {
|
|
399
|
+
prepareJsonResponse();
|
|
400
|
+
|
|
401
|
+
const abortController = new AbortController();
|
|
402
|
+
await createTestModel().doGenerate({
|
|
403
|
+
prompt: 'Test prompt',
|
|
404
|
+
files: undefined,
|
|
405
|
+
mask: undefined,
|
|
406
|
+
n: 1,
|
|
407
|
+
size: undefined,
|
|
408
|
+
aspectRatio: undefined,
|
|
409
|
+
seed: undefined,
|
|
410
|
+
abortSignal: abortController.signal,
|
|
411
|
+
providerOptions: {},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(server.calls.length).toBe(1);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should handle API errors correctly', async () => {
|
|
418
|
+
server.urls['https://api.test.com/image-model'].response = {
|
|
419
|
+
type: 'error',
|
|
420
|
+
status: 400,
|
|
421
|
+
body: JSON.stringify({
|
|
422
|
+
error: {
|
|
423
|
+
message: 'Invalid request',
|
|
424
|
+
code: 'invalid_request',
|
|
425
|
+
},
|
|
426
|
+
}),
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
await expect(
|
|
430
|
+
createTestModel().doGenerate({
|
|
431
|
+
prompt: 'Test prompt',
|
|
432
|
+
files: undefined,
|
|
433
|
+
mask: undefined,
|
|
434
|
+
n: 1,
|
|
435
|
+
size: undefined,
|
|
436
|
+
aspectRatio: undefined,
|
|
437
|
+
seed: undefined,
|
|
438
|
+
providerOptions: {},
|
|
439
|
+
}),
|
|
440
|
+
).rejects.toThrow();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should handle authentication errors', async () => {
|
|
444
|
+
server.urls['https://api.test.com/image-model'].response = {
|
|
445
|
+
type: 'error',
|
|
446
|
+
status: 401,
|
|
447
|
+
body: JSON.stringify({
|
|
448
|
+
error: {
|
|
449
|
+
message: 'Unauthorized',
|
|
450
|
+
code: 'unauthorized',
|
|
451
|
+
},
|
|
452
|
+
}),
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
await expect(
|
|
456
|
+
createTestModel().doGenerate({
|
|
457
|
+
prompt: 'Test prompt',
|
|
458
|
+
files: undefined,
|
|
459
|
+
mask: undefined,
|
|
460
|
+
n: 1,
|
|
461
|
+
size: undefined,
|
|
462
|
+
aspectRatio: undefined,
|
|
463
|
+
seed: undefined,
|
|
464
|
+
providerOptions: {},
|
|
465
|
+
}),
|
|
466
|
+
).rejects.toThrow();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should include providerOptions object in request body', async () => {
|
|
470
|
+
prepareJsonResponse();
|
|
471
|
+
|
|
472
|
+
await createTestModel().doGenerate({
|
|
473
|
+
prompt: 'Test prompt',
|
|
474
|
+
files: undefined,
|
|
475
|
+
mask: undefined,
|
|
476
|
+
n: 1,
|
|
477
|
+
size: undefined,
|
|
478
|
+
aspectRatio: undefined,
|
|
479
|
+
seed: undefined,
|
|
480
|
+
providerOptions: {
|
|
481
|
+
vertex: {
|
|
482
|
+
safetySettings: 'block_none',
|
|
483
|
+
},
|
|
484
|
+
openai: {
|
|
485
|
+
style: 'vivid',
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
491
|
+
expect(requestBody).toEqual({
|
|
492
|
+
prompt: 'Test prompt',
|
|
493
|
+
n: 1,
|
|
494
|
+
providerOptions: {
|
|
495
|
+
vertex: {
|
|
496
|
+
safetySettings: 'block_none',
|
|
497
|
+
},
|
|
498
|
+
openai: {
|
|
499
|
+
style: 'vivid',
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should handle empty provider options', async () => {
|
|
506
|
+
prepareJsonResponse();
|
|
507
|
+
|
|
508
|
+
await createTestModel().doGenerate({
|
|
509
|
+
prompt: 'Test prompt',
|
|
510
|
+
files: undefined,
|
|
511
|
+
mask: undefined,
|
|
512
|
+
n: 1,
|
|
513
|
+
size: undefined,
|
|
514
|
+
aspectRatio: undefined,
|
|
515
|
+
seed: undefined,
|
|
516
|
+
providerOptions: {},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
520
|
+
expect(requestBody).toEqual({
|
|
521
|
+
prompt: 'Test prompt',
|
|
522
|
+
n: 1,
|
|
523
|
+
providerOptions: {},
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should handle different model IDs', async () => {
|
|
528
|
+
prepareJsonResponse();
|
|
529
|
+
|
|
530
|
+
const customModelId = 'openai/dall-e-3';
|
|
531
|
+
const model = new GatewayImageModel(customModelId, {
|
|
532
|
+
provider: 'gateway',
|
|
533
|
+
baseURL: 'https://api.test.com',
|
|
534
|
+
headers: async () => ({}),
|
|
535
|
+
fetch: globalThis.fetch,
|
|
536
|
+
o11yHeaders: {},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await model.doGenerate({
|
|
540
|
+
prompt: 'Test prompt',
|
|
541
|
+
files: undefined,
|
|
542
|
+
mask: undefined,
|
|
543
|
+
n: 1,
|
|
544
|
+
size: undefined,
|
|
545
|
+
aspectRatio: undefined,
|
|
546
|
+
seed: undefined,
|
|
547
|
+
providerOptions: {},
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const headers = server.calls[0].requestHeaders;
|
|
551
|
+
expect(headers).toMatchObject({
|
|
552
|
+
'ai-model-id': customModelId,
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('should handle complex provider metadata with multiple providers', async () => {
|
|
557
|
+
prepareJsonResponse({
|
|
558
|
+
images: ['base64-1', 'base64-2'],
|
|
559
|
+
providerMetadata: {
|
|
560
|
+
vertex: {
|
|
561
|
+
images: [
|
|
562
|
+
{ revisedPrompt: 'Revised 1' },
|
|
563
|
+
{ revisedPrompt: 'Revised 2' },
|
|
564
|
+
],
|
|
565
|
+
usage: { tokens: 150 },
|
|
566
|
+
},
|
|
567
|
+
gateway: {
|
|
568
|
+
routing: {
|
|
569
|
+
provider: 'vertex',
|
|
570
|
+
attempts: [
|
|
571
|
+
{ provider: 'openai', success: false },
|
|
572
|
+
{ provider: 'vertex', success: true },
|
|
573
|
+
],
|
|
574
|
+
},
|
|
575
|
+
cost: '0.08',
|
|
576
|
+
marketCost: '0.12',
|
|
577
|
+
generationId: 'gen-xyz-789',
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const result = await createTestModel().doGenerate({
|
|
583
|
+
prompt: 'Test prompt',
|
|
584
|
+
files: undefined,
|
|
585
|
+
mask: undefined,
|
|
586
|
+
n: 2,
|
|
587
|
+
size: undefined,
|
|
588
|
+
aspectRatio: undefined,
|
|
589
|
+
seed: undefined,
|
|
590
|
+
providerOptions: {},
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
expect(result.providerMetadata).toEqual({
|
|
594
|
+
vertex: {
|
|
595
|
+
images: [
|
|
596
|
+
{ revisedPrompt: 'Revised 1' },
|
|
597
|
+
{ revisedPrompt: 'Revised 2' },
|
|
598
|
+
],
|
|
599
|
+
usage: { tokens: 150 },
|
|
600
|
+
},
|
|
601
|
+
gateway: {
|
|
602
|
+
routing: {
|
|
603
|
+
provider: 'vertex',
|
|
604
|
+
attempts: [
|
|
605
|
+
{ provider: 'openai', success: false },
|
|
606
|
+
{ provider: 'vertex', success: true },
|
|
607
|
+
],
|
|
608
|
+
},
|
|
609
|
+
cost: '0.08',
|
|
610
|
+
marketCost: '0.12',
|
|
611
|
+
generationId: 'gen-xyz-789',
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
describe('file encoding', () => {
|
|
617
|
+
it('should encode Uint8Array files to base64 strings', async () => {
|
|
618
|
+
prepareJsonResponse();
|
|
619
|
+
|
|
620
|
+
const binaryData = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
|
|
621
|
+
|
|
622
|
+
await createTestModel().doGenerate({
|
|
623
|
+
prompt: 'Edit this image',
|
|
624
|
+
files: [
|
|
625
|
+
{
|
|
626
|
+
type: 'file',
|
|
627
|
+
mediaType: 'image/png',
|
|
628
|
+
data: binaryData,
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
mask: undefined,
|
|
632
|
+
n: 1,
|
|
633
|
+
size: undefined,
|
|
634
|
+
aspectRatio: undefined,
|
|
635
|
+
seed: undefined,
|
|
636
|
+
providerOptions: {},
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
640
|
+
expect(requestBody.files).toHaveLength(1);
|
|
641
|
+
expect(requestBody.files[0]).toEqual({
|
|
642
|
+
type: 'file',
|
|
643
|
+
mediaType: 'image/png',
|
|
644
|
+
data: 'SGVsbG8=', // "Hello" in base64
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('should pass through files with string data unchanged', async () => {
|
|
649
|
+
prepareJsonResponse();
|
|
650
|
+
|
|
651
|
+
await createTestModel().doGenerate({
|
|
652
|
+
prompt: 'Edit this image',
|
|
653
|
+
files: [
|
|
654
|
+
{
|
|
655
|
+
type: 'file',
|
|
656
|
+
mediaType: 'image/png',
|
|
657
|
+
data: 'already-base64-encoded',
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
mask: undefined,
|
|
661
|
+
n: 1,
|
|
662
|
+
size: undefined,
|
|
663
|
+
aspectRatio: undefined,
|
|
664
|
+
seed: undefined,
|
|
665
|
+
providerOptions: {},
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
669
|
+
expect(requestBody.files).toHaveLength(1);
|
|
670
|
+
expect(requestBody.files[0]).toEqual({
|
|
671
|
+
type: 'file',
|
|
672
|
+
mediaType: 'image/png',
|
|
673
|
+
data: 'already-base64-encoded',
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('should pass through URL-type files unchanged', async () => {
|
|
678
|
+
prepareJsonResponse();
|
|
679
|
+
|
|
680
|
+
await createTestModel().doGenerate({
|
|
681
|
+
prompt: 'Edit this image',
|
|
682
|
+
files: [
|
|
683
|
+
{
|
|
684
|
+
type: 'url',
|
|
685
|
+
url: 'https://example.com/image.png',
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
mask: undefined,
|
|
689
|
+
n: 1,
|
|
690
|
+
size: undefined,
|
|
691
|
+
aspectRatio: undefined,
|
|
692
|
+
seed: undefined,
|
|
693
|
+
providerOptions: {},
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
697
|
+
expect(requestBody.files).toHaveLength(1);
|
|
698
|
+
expect(requestBody.files[0]).toEqual({
|
|
699
|
+
type: 'url',
|
|
700
|
+
url: 'https://example.com/image.png',
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('should encode Uint8Array mask to base64 string', async () => {
|
|
705
|
+
prepareJsonResponse();
|
|
706
|
+
|
|
707
|
+
const maskData = new Uint8Array([255, 0, 255, 0]); // Simple mask
|
|
708
|
+
|
|
709
|
+
await createTestModel().doGenerate({
|
|
710
|
+
prompt: 'Inpaint this area',
|
|
711
|
+
files: undefined,
|
|
712
|
+
mask: {
|
|
713
|
+
type: 'file',
|
|
714
|
+
mediaType: 'image/png',
|
|
715
|
+
data: maskData,
|
|
716
|
+
},
|
|
717
|
+
n: 1,
|
|
718
|
+
size: undefined,
|
|
719
|
+
aspectRatio: undefined,
|
|
720
|
+
seed: undefined,
|
|
721
|
+
providerOptions: {},
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
725
|
+
expect(requestBody.mask).toEqual({
|
|
726
|
+
type: 'file',
|
|
727
|
+
mediaType: 'image/png',
|
|
728
|
+
data: '/wD/AA==', // [255, 0, 255, 0] in base64
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('should handle mixed file types with encoding', async () => {
|
|
733
|
+
prepareJsonResponse();
|
|
734
|
+
|
|
735
|
+
const binaryData = new Uint8Array([1, 2, 3]);
|
|
736
|
+
|
|
737
|
+
await createTestModel().doGenerate({
|
|
738
|
+
prompt: 'Edit these images',
|
|
739
|
+
files: [
|
|
740
|
+
{
|
|
741
|
+
type: 'file',
|
|
742
|
+
mediaType: 'image/png',
|
|
743
|
+
data: binaryData,
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
type: 'file',
|
|
747
|
+
mediaType: 'image/jpeg',
|
|
748
|
+
data: 'already-encoded',
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
type: 'url',
|
|
752
|
+
url: 'https://example.com/image.png',
|
|
753
|
+
},
|
|
754
|
+
],
|
|
755
|
+
mask: {
|
|
756
|
+
type: 'file',
|
|
757
|
+
mediaType: 'image/png',
|
|
758
|
+
data: new Uint8Array([4, 5, 6]),
|
|
759
|
+
},
|
|
760
|
+
n: 1,
|
|
761
|
+
size: undefined,
|
|
762
|
+
aspectRatio: undefined,
|
|
763
|
+
seed: undefined,
|
|
764
|
+
providerOptions: {},
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
768
|
+
expect(requestBody.files).toHaveLength(3);
|
|
769
|
+
expect(requestBody.files[0]).toEqual({
|
|
770
|
+
type: 'file',
|
|
771
|
+
mediaType: 'image/png',
|
|
772
|
+
data: 'AQID', // [1, 2, 3] in base64
|
|
773
|
+
});
|
|
774
|
+
expect(requestBody.files[1]).toEqual({
|
|
775
|
+
type: 'file',
|
|
776
|
+
mediaType: 'image/jpeg',
|
|
777
|
+
data: 'already-encoded',
|
|
778
|
+
});
|
|
779
|
+
expect(requestBody.files[2]).toEqual({
|
|
780
|
+
type: 'url',
|
|
781
|
+
url: 'https://example.com/image.png',
|
|
782
|
+
});
|
|
783
|
+
expect(requestBody.mask).toEqual({
|
|
784
|
+
type: 'file',
|
|
785
|
+
mediaType: 'image/png',
|
|
786
|
+
data: 'BAUG', // [4, 5, 6] in base64
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('should preserve providerOptions on files during encoding', async () => {
|
|
791
|
+
prepareJsonResponse();
|
|
792
|
+
|
|
793
|
+
const binaryData = new Uint8Array([72, 101, 108, 108, 111]);
|
|
794
|
+
|
|
795
|
+
await createTestModel().doGenerate({
|
|
796
|
+
prompt: 'Edit this image',
|
|
797
|
+
files: [
|
|
798
|
+
{
|
|
799
|
+
type: 'file',
|
|
800
|
+
mediaType: 'image/png',
|
|
801
|
+
data: binaryData,
|
|
802
|
+
providerOptions: { openai: { quality: 'hd' } },
|
|
803
|
+
},
|
|
804
|
+
],
|
|
805
|
+
mask: undefined,
|
|
806
|
+
n: 1,
|
|
807
|
+
size: undefined,
|
|
808
|
+
aspectRatio: undefined,
|
|
809
|
+
seed: undefined,
|
|
810
|
+
providerOptions: {},
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const requestBody = await server.calls[0].requestBodyJson;
|
|
814
|
+
expect(requestBody.files[0]).toEqual({
|
|
815
|
+
type: 'file',
|
|
816
|
+
mediaType: 'image/png',
|
|
817
|
+
data: 'SGVsbG8=',
|
|
818
|
+
providerOptions: { openai: { quality: 'hd' } },
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
});
|