@ai-sdk/gateway 0.0.0-64aae7dd-20260114144918 → 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +49 -4
  2. package/dist/index.d.mts +20 -10
  3. package/dist/index.d.ts +20 -10
  4. package/dist/index.js +62 -25
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +62 -25
  7. package/dist/index.mjs.map +1 -1
  8. package/docs/00-ai-gateway.mdx +625 -0
  9. package/package.json +12 -5
  10. package/src/errors/as-gateway-error.ts +33 -0
  11. package/src/errors/create-gateway-error.test.ts +590 -0
  12. package/src/errors/create-gateway-error.ts +132 -0
  13. package/src/errors/extract-api-call-response.test.ts +270 -0
  14. package/src/errors/extract-api-call-response.ts +15 -0
  15. package/src/errors/gateway-authentication-error.ts +84 -0
  16. package/src/errors/gateway-error-types.test.ts +278 -0
  17. package/src/errors/gateway-error.ts +47 -0
  18. package/src/errors/gateway-internal-server-error.ts +33 -0
  19. package/src/errors/gateway-invalid-request-error.ts +33 -0
  20. package/src/errors/gateway-model-not-found-error.ts +47 -0
  21. package/src/errors/gateway-rate-limit-error.ts +33 -0
  22. package/src/errors/gateway-response-error.ts +42 -0
  23. package/src/errors/index.ts +16 -0
  24. package/src/errors/parse-auth-method.test.ts +136 -0
  25. package/src/errors/parse-auth-method.ts +23 -0
  26. package/src/gateway-config.ts +7 -0
  27. package/src/gateway-embedding-model-settings.ts +22 -0
  28. package/src/gateway-embedding-model.test.ts +213 -0
  29. package/src/gateway-embedding-model.ts +109 -0
  30. package/src/gateway-fetch-metadata.test.ts +774 -0
  31. package/src/gateway-fetch-metadata.ts +127 -0
  32. package/src/gateway-image-model-settings.ts +12 -0
  33. package/src/gateway-image-model.test.ts +823 -0
  34. package/src/gateway-image-model.ts +145 -0
  35. package/src/gateway-language-model-settings.ts +159 -0
  36. package/src/gateway-language-model.test.ts +1485 -0
  37. package/src/gateway-language-model.ts +212 -0
  38. package/src/gateway-model-entry.ts +58 -0
  39. package/src/gateway-provider-options.ts +66 -0
  40. package/src/gateway-provider.test.ts +1210 -0
  41. package/src/gateway-provider.ts +284 -0
  42. package/src/gateway-tools.ts +15 -0
  43. package/src/index.ts +27 -0
  44. package/src/tool/perplexity-search.ts +294 -0
  45. package/src/vercel-environment.test.ts +65 -0
  46. package/src/vercel-environment.ts +6 -0
  47. 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
+ });