@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,1210 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
gateway,
|
|
4
|
+
createGatewayProvider,
|
|
5
|
+
getGatewayAuthToken,
|
|
6
|
+
} from './gateway-provider';
|
|
7
|
+
import { GatewayFetchMetadata } from './gateway-fetch-metadata';
|
|
8
|
+
import { NoSuchModelError } from '@ai-sdk/provider';
|
|
9
|
+
import { GatewayEmbeddingModel } from './gateway-embedding-model';
|
|
10
|
+
import { GatewayImageModel } from './gateway-image-model';
|
|
11
|
+
import { getVercelOidcToken, getVercelRequestId } from './vercel-environment';
|
|
12
|
+
import { resolve } from '@ai-sdk/provider-utils';
|
|
13
|
+
import { GatewayLanguageModel } from './gateway-language-model';
|
|
14
|
+
import {
|
|
15
|
+
GatewayAuthenticationError,
|
|
16
|
+
GatewayInternalServerError,
|
|
17
|
+
} from './errors';
|
|
18
|
+
import { fail } from 'node:assert';
|
|
19
|
+
|
|
20
|
+
vi.mock('./gateway-language-model', () => ({
|
|
21
|
+
GatewayLanguageModel: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// Mock the gateway fetch metadata to prevent actual network calls
|
|
25
|
+
// We'll create a more flexible mock that can simulate auth failures
|
|
26
|
+
const mockGetAvailableModels = vi.fn();
|
|
27
|
+
const mockGetCredits = vi.fn();
|
|
28
|
+
vi.mock('./gateway-fetch-metadata', () => ({
|
|
29
|
+
GatewayFetchMetadata: vi.fn().mockImplementation((config: any) => ({
|
|
30
|
+
getAvailableModels: async () => {
|
|
31
|
+
// Call the headers function to trigger authentication logic
|
|
32
|
+
if (config.headers && typeof config.headers === 'function') {
|
|
33
|
+
await config.headers();
|
|
34
|
+
}
|
|
35
|
+
return mockGetAvailableModels();
|
|
36
|
+
},
|
|
37
|
+
getCredits: async () => {
|
|
38
|
+
// Call the headers function to trigger authentication logic
|
|
39
|
+
if (config.headers && typeof config.headers === 'function') {
|
|
40
|
+
await config.headers();
|
|
41
|
+
}
|
|
42
|
+
return mockGetCredits();
|
|
43
|
+
},
|
|
44
|
+
})),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock('./vercel-environment', () => ({
|
|
48
|
+
getVercelOidcToken: vi.fn(),
|
|
49
|
+
getVercelRequestId: vi.fn(),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock('./version', () => ({
|
|
53
|
+
VERSION: '0.0.0-test',
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
type GatewayImageModelInternalConfig = {
|
|
57
|
+
provider: string;
|
|
58
|
+
baseURL: string;
|
|
59
|
+
headers: () => Promise<Record<string, string>>;
|
|
60
|
+
fetch?: typeof fetch;
|
|
61
|
+
o11yHeaders: () => Promise<Record<string, string>>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function assertIsGatewayImageModelInternalConfig(
|
|
65
|
+
value: unknown,
|
|
66
|
+
): asserts value is GatewayImageModelInternalConfig {
|
|
67
|
+
if (
|
|
68
|
+
!value ||
|
|
69
|
+
typeof value !== 'object' ||
|
|
70
|
+
typeof (value as { provider?: unknown }).provider !== 'string' ||
|
|
71
|
+
typeof (value as { baseURL?: unknown }).baseURL !== 'string' ||
|
|
72
|
+
typeof (value as { headers?: unknown }).headers !== 'function' ||
|
|
73
|
+
typeof (value as { o11yHeaders?: unknown }).o11yHeaders !== 'function'
|
|
74
|
+
) {
|
|
75
|
+
throw new Error('Invalid GatewayImageModel configuration');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getGatewayImageModelInternalConfig(
|
|
80
|
+
model: GatewayImageModel,
|
|
81
|
+
): GatewayImageModelInternalConfig {
|
|
82
|
+
const config = Reflect.get(model as object, 'config');
|
|
83
|
+
assertIsGatewayImageModelInternalConfig(config);
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('GatewayProvider', () => {
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
vi.mocked(getVercelOidcToken).mockResolvedValue('mock-oidc-token');
|
|
91
|
+
vi.mocked(getVercelRequestId).mockResolvedValue('mock-request-id');
|
|
92
|
+
// Set up default mock behavior for getAvailableModels and getCredits
|
|
93
|
+
mockGetAvailableModels.mockReturnValue({ models: [] });
|
|
94
|
+
mockGetCredits.mockReturnValue({ balance: '100.00', total_used: '50.00' });
|
|
95
|
+
if ('AI_GATEWAY_API_KEY' in process.env) {
|
|
96
|
+
Reflect.deleteProperty(process.env, 'AI_GATEWAY_API_KEY');
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('createGatewayProvider', () => {
|
|
101
|
+
it('should create provider with correct configuration', async () => {
|
|
102
|
+
const options = {
|
|
103
|
+
baseURL: 'https://api.example.com',
|
|
104
|
+
apiKey: 'test-api-key',
|
|
105
|
+
headers: { 'Custom-Header': 'value' },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const provider = createGatewayProvider(options);
|
|
109
|
+
provider('test-model');
|
|
110
|
+
|
|
111
|
+
expect(GatewayLanguageModel).toHaveBeenCalledWith(
|
|
112
|
+
'test-model',
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
provider: 'gateway',
|
|
115
|
+
baseURL: 'https://api.example.com',
|
|
116
|
+
headers: expect.any(Function),
|
|
117
|
+
fetch: undefined,
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Verify headers function
|
|
122
|
+
const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0];
|
|
123
|
+
const config = constructorCall[1];
|
|
124
|
+
const headers = await config.headers();
|
|
125
|
+
|
|
126
|
+
expect(headers).toEqual({
|
|
127
|
+
authorization: 'Bearer test-api-key',
|
|
128
|
+
'custom-header': 'value',
|
|
129
|
+
'ai-gateway-protocol-version': expect.any(String),
|
|
130
|
+
'ai-gateway-auth-method': 'api-key',
|
|
131
|
+
'user-agent': 'ai-sdk/gateway/0.0.0-test',
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should use OIDC token when no API key is provided', async () => {
|
|
136
|
+
const options = {
|
|
137
|
+
baseURL: 'https://api.example.com',
|
|
138
|
+
headers: { 'Custom-Header': 'value' },
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const provider = createGatewayProvider(options);
|
|
142
|
+
provider('test-model');
|
|
143
|
+
|
|
144
|
+
const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0];
|
|
145
|
+
const config = constructorCall[1];
|
|
146
|
+
const headers = await config.headers();
|
|
147
|
+
|
|
148
|
+
expect(headers).toEqual({
|
|
149
|
+
authorization: 'Bearer mock-oidc-token',
|
|
150
|
+
'custom-header': 'value',
|
|
151
|
+
'ai-gateway-protocol-version': expect.any(String),
|
|
152
|
+
'ai-gateway-auth-method': 'oidc',
|
|
153
|
+
'user-agent': 'ai-sdk/gateway/0.0.0-test',
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should throw error when instantiated with new keyword', () => {
|
|
158
|
+
const provider = createGatewayProvider({
|
|
159
|
+
baseURL: 'https://api.example.com',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(() => {
|
|
163
|
+
new (provider as unknown as {
|
|
164
|
+
(modelId: string): unknown;
|
|
165
|
+
new (modelId: string): never;
|
|
166
|
+
})('test-model');
|
|
167
|
+
}).toThrow(
|
|
168
|
+
'The Gateway Provider model function cannot be called with the new keyword.',
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should create GatewayEmbeddingModel for embeddingModel', () => {
|
|
173
|
+
const provider = createGatewayProvider({
|
|
174
|
+
baseURL: 'https://api.example.com',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const model = provider.embeddingModel('openai/text-embedding-3-small');
|
|
178
|
+
expect(model).toBeInstanceOf(GatewayEmbeddingModel);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should create GatewayImageModel for imageModel', () => {
|
|
182
|
+
const provider = createGatewayProvider({
|
|
183
|
+
baseURL: 'https://api.example.com',
|
|
184
|
+
apiKey: 'test-api-key',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const model = provider.imageModel('google/imagen-4.0-generate');
|
|
188
|
+
|
|
189
|
+
if (!(model instanceof GatewayImageModel)) {
|
|
190
|
+
fail('Expected GatewayImageModel to be created');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const config = getGatewayImageModelInternalConfig(model);
|
|
194
|
+
expect(config.provider).toBe('gateway');
|
|
195
|
+
expect(config.baseURL).toBe('https://api.example.com');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should reuse gateway headers and fetch for imageModel', async () => {
|
|
199
|
+
const customFetch = vi.fn();
|
|
200
|
+
const provider = createGatewayProvider({
|
|
201
|
+
baseURL: 'https://api.example.com',
|
|
202
|
+
apiKey: 'test-api-key',
|
|
203
|
+
headers: { 'Custom-Header': 'value' },
|
|
204
|
+
fetch: customFetch,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const model = provider.imageModel('google/imagen-4.0-generate');
|
|
208
|
+
|
|
209
|
+
if (!(model instanceof GatewayImageModel)) {
|
|
210
|
+
fail('Expected GatewayImageModel to be created');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const config = getGatewayImageModelInternalConfig(model);
|
|
214
|
+
const headers = await config.headers();
|
|
215
|
+
|
|
216
|
+
expect(headers).toEqual({
|
|
217
|
+
authorization: 'Bearer test-api-key',
|
|
218
|
+
'custom-header': 'value',
|
|
219
|
+
'ai-gateway-protocol-version': expect.any(String),
|
|
220
|
+
'ai-gateway-auth-method': 'api-key',
|
|
221
|
+
'user-agent': 'ai-sdk/gateway/0.0.0-test',
|
|
222
|
+
});
|
|
223
|
+
expect(config.fetch).toBe(customFetch);
|
|
224
|
+
|
|
225
|
+
const o11yHeaders = await config.o11yHeaders();
|
|
226
|
+
expect(o11yHeaders).toEqual({ 'ai-o11y-request-id': 'mock-request-id' });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should fetch available models', async () => {
|
|
230
|
+
mockGetAvailableModels.mockReturnValue({ models: [] });
|
|
231
|
+
|
|
232
|
+
const options = {
|
|
233
|
+
baseURL: 'https://api.example.com',
|
|
234
|
+
apiKey: 'test-api-key',
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const provider = createGatewayProvider(options);
|
|
238
|
+
await provider.getAvailableModels();
|
|
239
|
+
|
|
240
|
+
expect(GatewayFetchMetadata).toHaveBeenCalledWith(
|
|
241
|
+
expect.objectContaining({
|
|
242
|
+
baseURL: 'https://api.example.com',
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
expect(mockGetAvailableModels).toHaveBeenCalled();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('metadata caching', () => {
|
|
249
|
+
it('should cache metadata for the specified refresh interval', async () => {
|
|
250
|
+
mockGetAvailableModels.mockReturnValue({
|
|
251
|
+
models: [{ id: 'test-model', specification: {} }],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
let currentTime = new Date('2024-01-01T00:00:00Z').getTime();
|
|
255
|
+
const provider = createGatewayProvider({
|
|
256
|
+
baseURL: 'https://api.example.com',
|
|
257
|
+
metadataCacheRefreshMillis: 10000, // 10 seconds
|
|
258
|
+
_internal: {
|
|
259
|
+
currentDate: () => new Date(currentTime),
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// First call should fetch metadata
|
|
264
|
+
await provider.getAvailableModels();
|
|
265
|
+
expect(mockGetAvailableModels).toHaveBeenCalledTimes(1);
|
|
266
|
+
|
|
267
|
+
// Second immediate call should use cache
|
|
268
|
+
await provider.getAvailableModels();
|
|
269
|
+
expect(mockGetAvailableModels).toHaveBeenCalledTimes(1);
|
|
270
|
+
|
|
271
|
+
// Advance time by 9 seconds (should still use cache)
|
|
272
|
+
currentTime += 9000;
|
|
273
|
+
await provider.getAvailableModels();
|
|
274
|
+
expect(mockGetAvailableModels).toHaveBeenCalledTimes(1);
|
|
275
|
+
|
|
276
|
+
// Advance time past 10 seconds (should refresh)
|
|
277
|
+
currentTime += 2000;
|
|
278
|
+
await provider.getAvailableModels();
|
|
279
|
+
expect(mockGetAvailableModels).toHaveBeenCalledTimes(2);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should use default 5 minute refresh interval when not specified', async () => {
|
|
283
|
+
mockGetAvailableModels.mockReturnValue({
|
|
284
|
+
models: [{ id: 'test-model', specification: {} }],
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
let currentTime = new Date('2024-01-01T00:00:00Z').getTime();
|
|
288
|
+
const provider = createGatewayProvider({
|
|
289
|
+
baseURL: 'https://api.example.com',
|
|
290
|
+
_internal: {
|
|
291
|
+
currentDate: () => new Date(currentTime),
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// First call should fetch metadata
|
|
296
|
+
await provider.getAvailableModels();
|
|
297
|
+
expect(mockGetAvailableModels).toHaveBeenCalledTimes(1);
|
|
298
|
+
|
|
299
|
+
// Advance time by 4 minutes (should still use cache)
|
|
300
|
+
currentTime += 4 * 60 * 1000;
|
|
301
|
+
await provider.getAvailableModels();
|
|
302
|
+
expect(mockGetAvailableModels).toHaveBeenCalledTimes(1);
|
|
303
|
+
|
|
304
|
+
// Advance time past 5 minutes (should refresh)
|
|
305
|
+
currentTime += 2 * 60 * 1000;
|
|
306
|
+
await provider.getAvailableModels();
|
|
307
|
+
expect(mockGetAvailableModels).toHaveBeenCalledTimes(2);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should pass o11y headers to GatewayLanguageModel when environment variables are set', async () => {
|
|
312
|
+
const originalEnv = process.env;
|
|
313
|
+
process.env = {
|
|
314
|
+
...originalEnv,
|
|
315
|
+
VERCEL_DEPLOYMENT_ID: 'test-deployment',
|
|
316
|
+
VERCEL_ENV: 'test',
|
|
317
|
+
VERCEL_REGION: 'iad1',
|
|
318
|
+
};
|
|
319
|
+
vi.mocked(getVercelRequestId).mockResolvedValue('test-request-id');
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const provider = createGatewayProvider({
|
|
323
|
+
baseURL: 'https://api.example.com',
|
|
324
|
+
apiKey: 'test-api-key',
|
|
325
|
+
});
|
|
326
|
+
provider('test-model');
|
|
327
|
+
|
|
328
|
+
const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0];
|
|
329
|
+
const config = constructorCall[1];
|
|
330
|
+
|
|
331
|
+
expect(config).toEqual(
|
|
332
|
+
expect.objectContaining({
|
|
333
|
+
provider: 'gateway',
|
|
334
|
+
baseURL: 'https://api.example.com',
|
|
335
|
+
o11yHeaders: expect.any(Function),
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Test that the o11yHeaders function returns the expected result
|
|
340
|
+
const o11yHeaders = await resolve(config.o11yHeaders);
|
|
341
|
+
expect(o11yHeaders).toEqual({
|
|
342
|
+
'ai-o11y-deployment-id': 'test-deployment',
|
|
343
|
+
'ai-o11y-environment': 'test',
|
|
344
|
+
'ai-o11y-region': 'iad1',
|
|
345
|
+
'ai-o11y-request-id': 'test-request-id',
|
|
346
|
+
});
|
|
347
|
+
} finally {
|
|
348
|
+
process.env = originalEnv;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should not include undefined o11y headers', async () => {
|
|
353
|
+
const originalEnv = process.env;
|
|
354
|
+
process.env = { ...originalEnv };
|
|
355
|
+
process.env.VERCEL_DEPLOYMENT_ID = undefined;
|
|
356
|
+
process.env.VERCEL_ENV = undefined;
|
|
357
|
+
process.env.VERCEL_REGION = undefined;
|
|
358
|
+
|
|
359
|
+
vi.mocked(getVercelRequestId).mockResolvedValue(undefined);
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const provider = createGatewayProvider({
|
|
363
|
+
baseURL: 'https://api.example.com',
|
|
364
|
+
apiKey: 'test-api-key',
|
|
365
|
+
});
|
|
366
|
+
provider('test-model');
|
|
367
|
+
|
|
368
|
+
// Get the constructor call to check o11yHeaders
|
|
369
|
+
const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0];
|
|
370
|
+
const config = constructorCall[1];
|
|
371
|
+
|
|
372
|
+
expect(config).toEqual(
|
|
373
|
+
expect.objectContaining({
|
|
374
|
+
provider: 'gateway',
|
|
375
|
+
baseURL: 'https://api.example.com',
|
|
376
|
+
o11yHeaders: expect.any(Function),
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Test that the o11yHeaders function returns empty object
|
|
381
|
+
const o11yHeaders = await resolve(config.o11yHeaders);
|
|
382
|
+
expect(o11yHeaders).toEqual({});
|
|
383
|
+
} finally {
|
|
384
|
+
process.env = originalEnv;
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('default exported provider', () => {
|
|
390
|
+
it('should export a default provider instance', () => {
|
|
391
|
+
expect(gateway).toBeDefined();
|
|
392
|
+
expect(typeof gateway).toBe('function');
|
|
393
|
+
expect(typeof gateway.languageModel).toBe('function');
|
|
394
|
+
expect(typeof gateway.getAvailableModels).toBe('function');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should use the default baseURL when none is provided', async () => {
|
|
398
|
+
// Set up mock to return empty models
|
|
399
|
+
mockGetAvailableModels.mockReturnValue({ models: [] });
|
|
400
|
+
|
|
401
|
+
// Create a provider without specifying baseURL
|
|
402
|
+
const testProvider = createGatewayProvider({
|
|
403
|
+
apiKey: 'test-key', // Provide API key to avoid OIDC token lookup
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Trigger a request
|
|
407
|
+
await testProvider.getAvailableModels();
|
|
408
|
+
|
|
409
|
+
// Check that GatewayFetchMetadata was instantiated with the default baseURL
|
|
410
|
+
expect(GatewayFetchMetadata).toHaveBeenCalledWith(
|
|
411
|
+
expect.objectContaining({
|
|
412
|
+
baseURL: 'https://ai-gateway.vercel.sh/v3/ai',
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should accept empty options', () => {
|
|
418
|
+
// This should not throw an error
|
|
419
|
+
const provider = createGatewayProvider();
|
|
420
|
+
expect(provider).toBeDefined();
|
|
421
|
+
expect(typeof provider).toBe('function');
|
|
422
|
+
expect(typeof provider.languageModel).toBe('function');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should expose imageModel on the default provider and construct model', () => {
|
|
426
|
+
expect(typeof gateway.imageModel).toBe('function');
|
|
427
|
+
const model = gateway.imageModel('google/imagen-4.0-generate');
|
|
428
|
+
|
|
429
|
+
if (!(model instanceof GatewayImageModel)) {
|
|
430
|
+
fail('Expected GatewayImageModel to be created by default provider');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const config = getGatewayImageModelInternalConfig(model);
|
|
434
|
+
expect(config.provider).toBe('gateway');
|
|
435
|
+
expect(config.baseURL).toBe('https://ai-gateway.vercel.sh/v3/ai');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should override default baseURL when provided', async () => {
|
|
439
|
+
// Reset mocks
|
|
440
|
+
vi.clearAllMocks();
|
|
441
|
+
|
|
442
|
+
// Set up mock to return empty models
|
|
443
|
+
mockGetAvailableModels.mockReturnValue({ models: [] });
|
|
444
|
+
|
|
445
|
+
const customBaseUrl = 'https://custom-api.example.com';
|
|
446
|
+
const testProvider = createGatewayProvider({
|
|
447
|
+
baseURL: customBaseUrl,
|
|
448
|
+
apiKey: 'test-key',
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Trigger a request
|
|
452
|
+
await testProvider.getAvailableModels();
|
|
453
|
+
|
|
454
|
+
// Check that GatewayFetchMetadata was instantiated with the custom baseURL
|
|
455
|
+
expect(GatewayFetchMetadata).toHaveBeenCalledWith(
|
|
456
|
+
expect.objectContaining({
|
|
457
|
+
baseURL: customBaseUrl,
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
expect(mockGetAvailableModels).toHaveBeenCalled();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should use apiKey over OIDC token when provided', async () => {
|
|
464
|
+
// Reset the mocks
|
|
465
|
+
vi.clearAllMocks();
|
|
466
|
+
|
|
467
|
+
// Mock getVercelOidcToken to ensure it's not called
|
|
468
|
+
vi.mocked(getVercelOidcToken).mockRejectedValue(
|
|
469
|
+
new Error('Should not be called'),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
// Set up mock to return empty models
|
|
473
|
+
mockGetAvailableModels.mockReturnValue({ models: [] });
|
|
474
|
+
|
|
475
|
+
const testApiKey = 'test-api-key-123';
|
|
476
|
+
const testProvider = createGatewayProvider({
|
|
477
|
+
apiKey: testApiKey,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Trigger a request that will use the headers
|
|
481
|
+
await testProvider.getAvailableModels();
|
|
482
|
+
|
|
483
|
+
// Get the headers function that was passed to GatewayFetchMetadata
|
|
484
|
+
const config = vi.mocked(GatewayFetchMetadata).mock.calls[0][0];
|
|
485
|
+
const headers = await resolve(config.headers());
|
|
486
|
+
|
|
487
|
+
// Verify that the API key was used in the Authorization header
|
|
488
|
+
expect(headers['authorization']).toBe(`Bearer ${testApiKey}`);
|
|
489
|
+
expect(headers['ai-gateway-auth-method']).toBe('api-key');
|
|
490
|
+
expect(headers['user-agent']).toBe('ai-sdk/gateway/0.0.0-test');
|
|
491
|
+
|
|
492
|
+
// Verify getVercelOidcToken was never called
|
|
493
|
+
expect(getVercelOidcToken).not.toHaveBeenCalled();
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Test data for different authentication scenarios
|
|
498
|
+
const authTestCases = [
|
|
499
|
+
{
|
|
500
|
+
name: 'no auth at all',
|
|
501
|
+
envOidcToken: undefined,
|
|
502
|
+
envApiKey: undefined,
|
|
503
|
+
optionsApiKey: undefined,
|
|
504
|
+
oidcTokenMock: null, // Will throw error
|
|
505
|
+
expectSuccess: false,
|
|
506
|
+
expectedError: 'authentication',
|
|
507
|
+
description: 'No OIDC token or API key provided',
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: 'valid oidc, invalid api key',
|
|
511
|
+
envOidcToken: 'valid-oidc-token-12345',
|
|
512
|
+
envApiKey: undefined,
|
|
513
|
+
optionsApiKey: 'invalid-api-key',
|
|
514
|
+
oidcTokenMock: 'valid-oidc-token-12345',
|
|
515
|
+
expectSuccess: true,
|
|
516
|
+
expectedAuthMethod: 'api-key', // Options API key takes precedence
|
|
517
|
+
description: 'Valid OIDC in env, but options API key takes precedence',
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: 'invalid oidc, valid api key',
|
|
521
|
+
envOidcToken: 'invalid-oidc-token',
|
|
522
|
+
envApiKey: undefined,
|
|
523
|
+
optionsApiKey: 'gw_valid_api_key_12345',
|
|
524
|
+
oidcTokenMock: null, // Will throw error
|
|
525
|
+
expectSuccess: true,
|
|
526
|
+
expectedAuthMethod: 'api-key',
|
|
527
|
+
description: 'Invalid OIDC, but valid API key should work',
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: 'no oidc, invalid api key',
|
|
531
|
+
envOidcToken: undefined,
|
|
532
|
+
envApiKey: 'invalid-api-key',
|
|
533
|
+
optionsApiKey: undefined,
|
|
534
|
+
oidcTokenMock: null, // Will throw error
|
|
535
|
+
expectSuccess: true,
|
|
536
|
+
expectedAuthMethod: 'api-key',
|
|
537
|
+
description: 'No OIDC, but env API key should be used',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: 'no oidc, valid api key',
|
|
541
|
+
envOidcToken: undefined,
|
|
542
|
+
envApiKey: 'gw_valid_api_key_12345',
|
|
543
|
+
optionsApiKey: undefined,
|
|
544
|
+
oidcTokenMock: null, // Won't be called
|
|
545
|
+
expectSuccess: true,
|
|
546
|
+
expectedAuthMethod: 'api-key',
|
|
547
|
+
description: 'Valid API key in environment should work',
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: 'valid oidc, no api key',
|
|
551
|
+
envOidcToken: 'valid-oidc-token-12345',
|
|
552
|
+
envApiKey: undefined,
|
|
553
|
+
optionsApiKey: undefined,
|
|
554
|
+
oidcTokenMock: 'valid-oidc-token-12345',
|
|
555
|
+
expectSuccess: true,
|
|
556
|
+
expectedAuthMethod: 'oidc',
|
|
557
|
+
description: 'Valid OIDC token should work when no API key provided',
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: 'valid oidc, valid api key',
|
|
561
|
+
envOidcToken: 'valid-oidc-token-12345',
|
|
562
|
+
envApiKey: 'gw_valid_api_key_12345',
|
|
563
|
+
optionsApiKey: undefined,
|
|
564
|
+
oidcTokenMock: 'valid-oidc-token-12345',
|
|
565
|
+
expectSuccess: true,
|
|
566
|
+
expectedAuthMethod: 'api-key',
|
|
567
|
+
description:
|
|
568
|
+
'Both valid credentials - API key should take precedence over OIDC',
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: 'valid oidc, valid options api key',
|
|
572
|
+
envOidcToken: 'valid-oidc-token-12345',
|
|
573
|
+
envApiKey: undefined,
|
|
574
|
+
optionsApiKey: 'gw_valid_options_api_key_12345',
|
|
575
|
+
oidcTokenMock: 'valid-oidc-token-12345',
|
|
576
|
+
expectSuccess: true,
|
|
577
|
+
expectedAuthMethod: 'api-key',
|
|
578
|
+
description:
|
|
579
|
+
'Both valid credentials - options API key should take precedence over OIDC',
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
name: 'invalid oidc, no api key',
|
|
583
|
+
envOidcToken: 'invalid-oidc-token',
|
|
584
|
+
envApiKey: undefined,
|
|
585
|
+
optionsApiKey: undefined,
|
|
586
|
+
oidcTokenMock: null, // Will throw error
|
|
587
|
+
expectSuccess: false,
|
|
588
|
+
expectedError: 'authentication',
|
|
589
|
+
description: 'Invalid OIDC and no API key should fail',
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
name: 'invalid oidc, invalid api key',
|
|
593
|
+
envOidcToken: 'invalid-oidc-token',
|
|
594
|
+
envApiKey: 'invalid-api-key',
|
|
595
|
+
optionsApiKey: undefined,
|
|
596
|
+
oidcTokenMock: null, // Will throw error for OIDC
|
|
597
|
+
expectSuccess: true,
|
|
598
|
+
expectedAuthMethod: 'api-key', // Env API key is still used even if "invalid"
|
|
599
|
+
description: 'Environment API key takes precedence over OIDC failure',
|
|
600
|
+
},
|
|
601
|
+
];
|
|
602
|
+
|
|
603
|
+
describe('Authentication Comprehensive Tests', () => {
|
|
604
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
605
|
+
|
|
606
|
+
beforeEach(() => {
|
|
607
|
+
// Store original environment
|
|
608
|
+
originalEnv = process.env;
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
afterEach(() => {
|
|
612
|
+
// Restore original environment
|
|
613
|
+
process.env = originalEnv;
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
describe('getGatewayAuthToken function', () => {
|
|
617
|
+
authTestCases.forEach(testCase => {
|
|
618
|
+
it(`should handle ${testCase.name}`, async () => {
|
|
619
|
+
// Set up environment variables for this test case
|
|
620
|
+
process.env = { ...originalEnv };
|
|
621
|
+
|
|
622
|
+
// Only set environment variables if they have actual values
|
|
623
|
+
if (testCase.envOidcToken !== undefined) {
|
|
624
|
+
process.env.VERCEL_OIDC_TOKEN = testCase.envOidcToken;
|
|
625
|
+
} else {
|
|
626
|
+
delete process.env.VERCEL_OIDC_TOKEN;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (testCase.envApiKey !== undefined) {
|
|
630
|
+
process.env.AI_GATEWAY_API_KEY = testCase.envApiKey;
|
|
631
|
+
} else {
|
|
632
|
+
delete process.env.AI_GATEWAY_API_KEY;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Mock OIDC token behavior
|
|
636
|
+
if (testCase.oidcTokenMock) {
|
|
637
|
+
vi.mocked(getVercelOidcToken).mockResolvedValue(
|
|
638
|
+
testCase.oidcTokenMock,
|
|
639
|
+
);
|
|
640
|
+
} else {
|
|
641
|
+
vi.mocked(getVercelOidcToken).mockRejectedValue(
|
|
642
|
+
new GatewayAuthenticationError({
|
|
643
|
+
message: 'OIDC token not available',
|
|
644
|
+
statusCode: 401,
|
|
645
|
+
}),
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const options: any = {};
|
|
650
|
+
if (testCase.optionsApiKey) {
|
|
651
|
+
options.apiKey = testCase.optionsApiKey;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (testCase.expectSuccess) {
|
|
655
|
+
// Test successful cases
|
|
656
|
+
const result = await getGatewayAuthToken(options);
|
|
657
|
+
|
|
658
|
+
expect(result.authMethod).toBe(testCase.expectedAuthMethod);
|
|
659
|
+
|
|
660
|
+
if (testCase.expectedAuthMethod === 'api-key') {
|
|
661
|
+
const expectedToken =
|
|
662
|
+
testCase.optionsApiKey || testCase.envApiKey;
|
|
663
|
+
expect(result.token).toBe(expectedToken);
|
|
664
|
+
|
|
665
|
+
// If we used options API key, OIDC should not be called
|
|
666
|
+
if (testCase.optionsApiKey) {
|
|
667
|
+
expect(getVercelOidcToken).not.toHaveBeenCalled();
|
|
668
|
+
}
|
|
669
|
+
} else if (testCase.expectedAuthMethod === 'oidc') {
|
|
670
|
+
expect(result.token).toBe(testCase.oidcTokenMock);
|
|
671
|
+
expect(getVercelOidcToken).toHaveBeenCalled();
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
// Test failure cases - should throw when OIDC fails
|
|
675
|
+
await expect(getGatewayAuthToken(options)).rejects.toThrow();
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
describe('createGatewayProvider authentication', () => {
|
|
682
|
+
authTestCases.forEach(testCase => {
|
|
683
|
+
it(`should handle provider creation with ${testCase.name}`, async () => {
|
|
684
|
+
// Set up environment variables for this test case
|
|
685
|
+
process.env = { ...originalEnv };
|
|
686
|
+
|
|
687
|
+
// Only set environment variables if they have actual values
|
|
688
|
+
if (testCase.envOidcToken !== undefined) {
|
|
689
|
+
process.env.VERCEL_OIDC_TOKEN = testCase.envOidcToken;
|
|
690
|
+
} else {
|
|
691
|
+
delete process.env.VERCEL_OIDC_TOKEN;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (testCase.envApiKey !== undefined) {
|
|
695
|
+
process.env.AI_GATEWAY_API_KEY = testCase.envApiKey;
|
|
696
|
+
} else {
|
|
697
|
+
delete process.env.AI_GATEWAY_API_KEY;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Mock OIDC token behavior
|
|
701
|
+
if (testCase.oidcTokenMock) {
|
|
702
|
+
vi.mocked(getVercelOidcToken).mockResolvedValue(
|
|
703
|
+
testCase.oidcTokenMock,
|
|
704
|
+
);
|
|
705
|
+
} else {
|
|
706
|
+
vi.mocked(getVercelOidcToken).mockRejectedValue(
|
|
707
|
+
new GatewayAuthenticationError({
|
|
708
|
+
message: 'OIDC token not available',
|
|
709
|
+
statusCode: 401,
|
|
710
|
+
}),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const options: any = {
|
|
715
|
+
baseURL: 'https://test-gateway.example.com',
|
|
716
|
+
};
|
|
717
|
+
if (testCase.optionsApiKey) {
|
|
718
|
+
options.apiKey = testCase.optionsApiKey;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const provider = createGatewayProvider({
|
|
722
|
+
...options,
|
|
723
|
+
// Force no caching to ensure headers are called each time
|
|
724
|
+
metadataCacheRefreshMillis: 0,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
if (testCase.expectSuccess) {
|
|
728
|
+
// Ensure the mock succeeds for successful test cases
|
|
729
|
+
mockGetAvailableModels.mockReturnValue({ models: [] });
|
|
730
|
+
|
|
731
|
+
// Test that provider can get available models (which requires auth)
|
|
732
|
+
const models = await provider.getAvailableModels();
|
|
733
|
+
expect(models).toBeDefined();
|
|
734
|
+
|
|
735
|
+
// For OIDC tests, we need to verify the auth token function was called
|
|
736
|
+
// which is indirectly tested by checking if getVercelOidcToken was called
|
|
737
|
+
if (testCase.expectedAuthMethod === 'oidc') {
|
|
738
|
+
expect(getVercelOidcToken).toHaveBeenCalled();
|
|
739
|
+
} else if (
|
|
740
|
+
testCase.expectedAuthMethod === 'api-key' &&
|
|
741
|
+
testCase.optionsApiKey
|
|
742
|
+
) {
|
|
743
|
+
// If we used options API key, OIDC should not be called
|
|
744
|
+
expect(getVercelOidcToken).not.toHaveBeenCalled();
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
// For failure cases, mock the metadata fetch to throw auth error
|
|
748
|
+
mockGetAvailableModels.mockImplementation(() => {
|
|
749
|
+
throw new GatewayAuthenticationError({
|
|
750
|
+
message: 'Authentication failed',
|
|
751
|
+
statusCode: 401,
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Test failure cases
|
|
756
|
+
await expect(provider.getAvailableModels()).rejects.toThrow(
|
|
757
|
+
/authentication|token/i,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
describe('Environment variable edge cases', () => {
|
|
765
|
+
it('should handle empty string environment variables as undefined', async () => {
|
|
766
|
+
process.env = {
|
|
767
|
+
...originalEnv,
|
|
768
|
+
VERCEL_OIDC_TOKEN: '',
|
|
769
|
+
AI_GATEWAY_API_KEY: '',
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
const oidcError = new GatewayAuthenticationError({
|
|
773
|
+
message: 'OIDC token not available',
|
|
774
|
+
statusCode: 401,
|
|
775
|
+
});
|
|
776
|
+
vi.mocked(getVercelOidcToken).mockRejectedValue(oidcError);
|
|
777
|
+
|
|
778
|
+
await expect(getGatewayAuthToken({})).rejects.toThrow(oidcError);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('should handle whitespace-only environment variables', async () => {
|
|
782
|
+
process.env = {
|
|
783
|
+
...originalEnv,
|
|
784
|
+
VERCEL_OIDC_TOKEN: ' ',
|
|
785
|
+
AI_GATEWAY_API_KEY: '\t\n ',
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// The whitespace API key should still be used (it's treated as a valid value)
|
|
789
|
+
const result = await getGatewayAuthToken({});
|
|
790
|
+
expect(result.authMethod).toBe('api-key');
|
|
791
|
+
expect(result.token).toBe('\t\n ');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('should prioritize options.apiKey over all environment variables', async () => {
|
|
795
|
+
process.env = {
|
|
796
|
+
...originalEnv,
|
|
797
|
+
VERCEL_OIDC_TOKEN: 'env-oidc-token',
|
|
798
|
+
AI_GATEWAY_API_KEY: 'env-api-key',
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const optionsApiKey = 'options-api-key';
|
|
802
|
+
const result = await getGatewayAuthToken({ apiKey: optionsApiKey });
|
|
803
|
+
|
|
804
|
+
expect(result.authMethod).toBe('api-key');
|
|
805
|
+
expect(result.token).toBe(optionsApiKey);
|
|
806
|
+
expect(getVercelOidcToken).not.toHaveBeenCalled();
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('should surface OIDC error as cause when authentication fails', async () => {
|
|
810
|
+
process.env = {
|
|
811
|
+
...originalEnv,
|
|
812
|
+
VERCEL_OIDC_TOKEN: '',
|
|
813
|
+
AI_GATEWAY_API_KEY: '',
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
delete process.env.AI_GATEWAY_API_KEY;
|
|
817
|
+
|
|
818
|
+
const oidcError = new Error(
|
|
819
|
+
'OIDC token generation failed: project not linked',
|
|
820
|
+
);
|
|
821
|
+
vi.mocked(getVercelOidcToken).mockRejectedValue(oidcError);
|
|
822
|
+
|
|
823
|
+
vi.mocked(GatewayFetchMetadata).mockImplementation(
|
|
824
|
+
(config: any) =>
|
|
825
|
+
({
|
|
826
|
+
getAvailableModels: async () => {
|
|
827
|
+
if (config.headers && typeof config.headers === 'function') {
|
|
828
|
+
await config.headers();
|
|
829
|
+
}
|
|
830
|
+
return mockGetAvailableModels();
|
|
831
|
+
},
|
|
832
|
+
getCredits: async () => {
|
|
833
|
+
if (config.headers && typeof config.headers === 'function') {
|
|
834
|
+
await config.headers();
|
|
835
|
+
}
|
|
836
|
+
return mockGetCredits();
|
|
837
|
+
},
|
|
838
|
+
}) as any,
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
const provider = createGatewayProvider();
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
await provider.getAvailableModels();
|
|
845
|
+
fail('Expected an error to be thrown');
|
|
846
|
+
} catch (error) {
|
|
847
|
+
expect(GatewayAuthenticationError.isInstance(error)).toBe(true);
|
|
848
|
+
if (GatewayAuthenticationError.isInstance(error)) {
|
|
849
|
+
expect(error.cause).toBe(oidcError);
|
|
850
|
+
expect(error.message).toContain('No authentication provided');
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
describe('Authentication precedence', () => {
|
|
857
|
+
it('should prefer options.apiKey over AI_GATEWAY_API_KEY', async () => {
|
|
858
|
+
process.env = {
|
|
859
|
+
...originalEnv,
|
|
860
|
+
AI_GATEWAY_API_KEY: 'env-api-key',
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
const optionsApiKey = 'options-api-key';
|
|
864
|
+
const result = await getGatewayAuthToken({ apiKey: optionsApiKey });
|
|
865
|
+
|
|
866
|
+
expect(result.authMethod).toBe('api-key');
|
|
867
|
+
expect(result.token).toBe(optionsApiKey);
|
|
868
|
+
expect(getVercelOidcToken).not.toHaveBeenCalled();
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it('should prefer AI_GATEWAY_API_KEY over OIDC token', async () => {
|
|
872
|
+
process.env = {
|
|
873
|
+
...originalEnv,
|
|
874
|
+
VERCEL_OIDC_TOKEN: 'oidc-token',
|
|
875
|
+
AI_GATEWAY_API_KEY: 'env-api-key',
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
const result = await getGatewayAuthToken({});
|
|
879
|
+
|
|
880
|
+
expect(result.authMethod).toBe('api-key');
|
|
881
|
+
expect(result.token).toBe('env-api-key');
|
|
882
|
+
expect(getVercelOidcToken).not.toHaveBeenCalled();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('should fall back to OIDC when no API keys are available', async () => {
|
|
886
|
+
process.env = {
|
|
887
|
+
...originalEnv,
|
|
888
|
+
VERCEL_OIDC_TOKEN: 'oidc-token',
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
vi.mocked(getVercelOidcToken).mockResolvedValue('oidc-token');
|
|
892
|
+
|
|
893
|
+
const result = await getGatewayAuthToken({});
|
|
894
|
+
|
|
895
|
+
expect(result.authMethod).toBe('oidc');
|
|
896
|
+
expect(result.token).toBe('oidc-token');
|
|
897
|
+
expect(getVercelOidcToken).toHaveBeenCalled();
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
describe('Real-world usage scenarios', () => {
|
|
902
|
+
it('should work in Vercel deployment with OIDC', async () => {
|
|
903
|
+
// Simulate Vercel deployment environment
|
|
904
|
+
process.env = {
|
|
905
|
+
...originalEnv,
|
|
906
|
+
VERCEL_OIDC_TOKEN: 'vercel-deployment-oidc-token',
|
|
907
|
+
VERCEL_DEPLOYMENT_ID: 'dpl_12345',
|
|
908
|
+
VERCEL_ENV: 'production',
|
|
909
|
+
VERCEL_REGION: 'iad1',
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// Explicitly remove AI_GATEWAY_API_KEY to force OIDC usage
|
|
913
|
+
delete process.env.AI_GATEWAY_API_KEY;
|
|
914
|
+
|
|
915
|
+
vi.mocked(getVercelOidcToken).mockResolvedValue(
|
|
916
|
+
'vercel-deployment-oidc-token',
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
const provider = createGatewayProvider();
|
|
920
|
+
const models = await provider.getAvailableModels();
|
|
921
|
+
|
|
922
|
+
expect(models).toBeDefined();
|
|
923
|
+
expect(getVercelOidcToken).toHaveBeenCalled();
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('should work in local development with API key', async () => {
|
|
927
|
+
// Simulate local development environment
|
|
928
|
+
process.env = {
|
|
929
|
+
...originalEnv,
|
|
930
|
+
AI_GATEWAY_API_KEY: 'local-dev-api-key',
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
const provider = createGatewayProvider();
|
|
934
|
+
const models = await provider.getAvailableModels();
|
|
935
|
+
|
|
936
|
+
expect(models).toBeDefined();
|
|
937
|
+
expect(getVercelOidcToken).not.toHaveBeenCalled();
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('should work with explicit API key override', async () => {
|
|
941
|
+
// User provides explicit API key, should override everything
|
|
942
|
+
process.env = {
|
|
943
|
+
...originalEnv,
|
|
944
|
+
VERCEL_OIDC_TOKEN: 'should-not-be-used',
|
|
945
|
+
AI_GATEWAY_API_KEY: 'should-not-be-used-either',
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
const explicitApiKey = 'explicit-user-api-key';
|
|
949
|
+
const provider = createGatewayProvider({
|
|
950
|
+
apiKey: explicitApiKey,
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
const models = await provider.getAvailableModels();
|
|
954
|
+
|
|
955
|
+
expect(models).toBeDefined();
|
|
956
|
+
expect(getVercelOidcToken).not.toHaveBeenCalled();
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
describe('getCredits method', () => {
|
|
962
|
+
it('should fetch credits successfully', async () => {
|
|
963
|
+
const mockCredits = { balance: '150.50', total_used: '75.25' };
|
|
964
|
+
mockGetCredits.mockReturnValue(mockCredits);
|
|
965
|
+
|
|
966
|
+
const provider = createGatewayProvider({
|
|
967
|
+
apiKey: 'test-key',
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
const credits = await provider.getCredits();
|
|
971
|
+
|
|
972
|
+
expect(credits).toEqual({ balance: '150.50', total_used: '75.25' });
|
|
973
|
+
expect(GatewayFetchMetadata).toHaveBeenCalledWith(
|
|
974
|
+
expect.objectContaining({
|
|
975
|
+
baseURL: 'https://ai-gateway.vercel.sh/v3/ai',
|
|
976
|
+
headers: expect.any(Function),
|
|
977
|
+
fetch: undefined,
|
|
978
|
+
}),
|
|
979
|
+
);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
it('should handle authentication errors in getCredits', async () => {
|
|
983
|
+
const provider = createGatewayProvider();
|
|
984
|
+
|
|
985
|
+
const result = await provider.getCredits();
|
|
986
|
+
expect(result).toEqual({ balance: '100.00', total_used: '50.00' });
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it('should work with custom baseURL', async () => {
|
|
990
|
+
const customBaseURL = 'https://custom-gateway.example.com/v3/ai';
|
|
991
|
+
const provider = createGatewayProvider({
|
|
992
|
+
apiKey: 'test-key',
|
|
993
|
+
baseURL: customBaseURL,
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
await provider.getCredits();
|
|
997
|
+
|
|
998
|
+
expect(GatewayFetchMetadata).toHaveBeenCalledWith(
|
|
999
|
+
expect.objectContaining({
|
|
1000
|
+
baseURL: customBaseURL,
|
|
1001
|
+
}),
|
|
1002
|
+
);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('should work with OIDC authentication', async () => {
|
|
1006
|
+
vi.mocked(getVercelOidcToken).mockResolvedValue('oidc-token');
|
|
1007
|
+
|
|
1008
|
+
const provider = createGatewayProvider();
|
|
1009
|
+
|
|
1010
|
+
const credits = await provider.getCredits();
|
|
1011
|
+
|
|
1012
|
+
expect(credits).toBeDefined();
|
|
1013
|
+
expect(getVercelOidcToken).toHaveBeenCalled();
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it('should handle errors from the credits endpoint', async () => {
|
|
1017
|
+
const testError = new Error('Credits service unavailable');
|
|
1018
|
+
mockGetCredits.mockRejectedValue(testError);
|
|
1019
|
+
|
|
1020
|
+
const provider = createGatewayProvider({
|
|
1021
|
+
apiKey: 'test-key',
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
await expect(provider.getCredits()).rejects.toThrow(
|
|
1025
|
+
'Credits service unavailable',
|
|
1026
|
+
);
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
it('should include proper headers for credits request', async () => {
|
|
1030
|
+
const provider = createGatewayProvider({
|
|
1031
|
+
apiKey: 'test-key',
|
|
1032
|
+
headers: { 'custom-header': 'custom-value' },
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
await provider.getCredits();
|
|
1036
|
+
|
|
1037
|
+
const config = vi.mocked(GatewayFetchMetadata).mock.calls[0][0];
|
|
1038
|
+
const headers = await config.headers();
|
|
1039
|
+
|
|
1040
|
+
expect(headers).toEqual({
|
|
1041
|
+
authorization: 'Bearer test-key',
|
|
1042
|
+
'ai-gateway-protocol-version': '0.0.1',
|
|
1043
|
+
'ai-gateway-auth-method': 'api-key',
|
|
1044
|
+
'custom-header': 'custom-value',
|
|
1045
|
+
'user-agent': 'ai-sdk/gateway/0.0.0-test',
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it('should be available on the provider interface', () => {
|
|
1050
|
+
const provider = createGatewayProvider({ apiKey: 'test-key' });
|
|
1051
|
+
expect(typeof provider.getCredits).toBe('function');
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
describe('Error handling in metadata fetching', () => {
|
|
1056
|
+
it('should convert metadata fetch errors to Gateway errors', async () => {
|
|
1057
|
+
mockGetAvailableModels.mockImplementation(() => {
|
|
1058
|
+
throw new GatewayInternalServerError({
|
|
1059
|
+
message: 'Database connection failed',
|
|
1060
|
+
statusCode: 500,
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
const provider = createGatewayProvider({
|
|
1065
|
+
baseURL: 'https://api.example.com',
|
|
1066
|
+
apiKey: 'test-key',
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
await expect(provider.getAvailableModels()).rejects.toMatchObject({
|
|
1070
|
+
name: 'GatewayInternalServerError',
|
|
1071
|
+
message: 'Database connection failed',
|
|
1072
|
+
statusCode: 500,
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it('should not double-wrap Gateway errors from metadata fetch', async () => {
|
|
1077
|
+
const originalError = new GatewayAuthenticationError({
|
|
1078
|
+
message: 'Invalid token',
|
|
1079
|
+
statusCode: 401,
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
mockGetAvailableModels.mockImplementation(() => {
|
|
1083
|
+
throw originalError;
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
const provider = createGatewayProvider({
|
|
1087
|
+
baseURL: 'https://api.example.com',
|
|
1088
|
+
apiKey: 'test-key',
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
try {
|
|
1092
|
+
await provider.getAvailableModels();
|
|
1093
|
+
fail('Expected error was not thrown');
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
expect(error).toBe(originalError); // Same instance
|
|
1096
|
+
expect(error).toBeInstanceOf(GatewayAuthenticationError);
|
|
1097
|
+
expect((error as GatewayAuthenticationError).message).toBe(
|
|
1098
|
+
'Invalid token',
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
it('should handle model specification errors', async () => {
|
|
1104
|
+
// Mock successful metadata fetch with a model
|
|
1105
|
+
mockGetAvailableModels.mockReturnValue({
|
|
1106
|
+
models: [
|
|
1107
|
+
{
|
|
1108
|
+
id: 'test-model',
|
|
1109
|
+
specification: {
|
|
1110
|
+
provider: 'test',
|
|
1111
|
+
specificationVersion: 'v2',
|
|
1112
|
+
modelId: 'test-model',
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
],
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
const provider = createGatewayProvider({
|
|
1119
|
+
baseURL: 'https://api.example.com',
|
|
1120
|
+
apiKey: 'test-key',
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
// Create a language model that should work
|
|
1124
|
+
const model = provider('test-model');
|
|
1125
|
+
expect(model).toBeDefined();
|
|
1126
|
+
|
|
1127
|
+
// Verify the model was created with the correct parameters
|
|
1128
|
+
expect(GatewayLanguageModel).toHaveBeenCalledWith(
|
|
1129
|
+
'test-model',
|
|
1130
|
+
expect.objectContaining({
|
|
1131
|
+
provider: 'gateway',
|
|
1132
|
+
baseURL: 'https://api.example.com',
|
|
1133
|
+
headers: expect.any(Function),
|
|
1134
|
+
fetch: undefined,
|
|
1135
|
+
o11yHeaders: expect.any(Function),
|
|
1136
|
+
}),
|
|
1137
|
+
);
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it('should create language model for any modelId', async () => {
|
|
1141
|
+
// Mock successful metadata fetch with different models
|
|
1142
|
+
mockGetAvailableModels.mockReturnValue({
|
|
1143
|
+
models: [
|
|
1144
|
+
{
|
|
1145
|
+
id: 'model-1',
|
|
1146
|
+
specification: {
|
|
1147
|
+
provider: 'test',
|
|
1148
|
+
specificationVersion: 'v2',
|
|
1149
|
+
modelId: 'model-1',
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1152
|
+
{
|
|
1153
|
+
id: 'model-2',
|
|
1154
|
+
specification: {
|
|
1155
|
+
provider: 'test',
|
|
1156
|
+
specificationVersion: 'v2',
|
|
1157
|
+
modelId: 'model-2',
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
],
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
const provider = createGatewayProvider({
|
|
1164
|
+
baseURL: 'https://api.example.com',
|
|
1165
|
+
apiKey: 'test-key',
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
// Create a language model for any model ID
|
|
1169
|
+
const model = provider('any-model-id');
|
|
1170
|
+
|
|
1171
|
+
// The model should be created successfully
|
|
1172
|
+
expect(GatewayLanguageModel).toHaveBeenCalledWith(
|
|
1173
|
+
'any-model-id',
|
|
1174
|
+
expect.objectContaining({
|
|
1175
|
+
provider: 'gateway',
|
|
1176
|
+
baseURL: 'https://api.example.com',
|
|
1177
|
+
headers: expect.any(Function),
|
|
1178
|
+
fetch: undefined,
|
|
1179
|
+
o11yHeaders: expect.any(Function),
|
|
1180
|
+
}),
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
expect(model).toBeDefined();
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
it('should handle non-existent model requests', async () => {
|
|
1187
|
+
const provider = createGatewayProvider({
|
|
1188
|
+
baseURL: 'https://api.example.com',
|
|
1189
|
+
apiKey: 'test-key',
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// Create a language model for a non-existent model
|
|
1193
|
+
const model = provider('non-existent-model');
|
|
1194
|
+
|
|
1195
|
+
// The model should be created successfully (validation happens at API call time)
|
|
1196
|
+
expect(GatewayLanguageModel).toHaveBeenCalledWith(
|
|
1197
|
+
'non-existent-model',
|
|
1198
|
+
expect.objectContaining({
|
|
1199
|
+
provider: 'gateway',
|
|
1200
|
+
baseURL: 'https://api.example.com',
|
|
1201
|
+
headers: expect.any(Function),
|
|
1202
|
+
fetch: undefined,
|
|
1203
|
+
o11yHeaders: expect.any(Function),
|
|
1204
|
+
}),
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
expect(model).toBeDefined();
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1210
|
+
});
|