@bernierllc/ai-provider-openai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +35 -0
- package/README.md +294 -0
- package/__tests__/OpenAIProvider.test.ts +574 -0
- package/__tests__/error-handling.test.ts +315 -0
- package/__tests__/model-registry.test.ts +270 -0
- package/__tests__/openai-specific.test.ts +333 -0
- package/dist/OpenAIProvider.d.ts +22 -0
- package/dist/OpenAIProvider.d.ts.map +1 -0
- package/dist/OpenAIProvider.js +320 -0
- package/dist/OpenAIProvider.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/models/model-registry.d.ts +8 -0
- package/dist/models/model-registry.d.ts.map +1 -0
- package/dist/models/model-registry.js +147 -0
- package/dist/models/model-registry.js.map +1 -0
- package/dist/types/openai-types.d.ts +28 -0
- package/dist/types/openai-types.d.ts.map +1 -0
- package/dist/types/openai-types.js +3 -0
- package/dist/types/openai-types.js.map +1 -0
- package/dist/utils/error-handling.d.ts +5 -0
- package/dist/utils/error-handling.d.ts.map +1 -0
- package/dist/utils/error-handling.js +67 -0
- package/dist/utils/error-handling.js.map +1 -0
- package/jest.config.cjs +29 -0
- package/package.json +63 -0
- package/src/OpenAIProvider.ts +435 -0
- package/src/index.ts +12 -0
- package/src/models/model-registry.ts +178 -0
- package/src/types/openai-types.ts +51 -0
- package/src/utils/error-handling.ts +101 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code only within the scope of the project it was delivered for.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { handleOpenAIError, isRetryableError, getRetryDelay } from '../src/utils/error-handling';
|
|
10
|
+
import { AIProviderError } from '@bernierllc/ai-provider-core';
|
|
11
|
+
import OpenAI from 'openai';
|
|
12
|
+
|
|
13
|
+
describe('Error Handling', () => {
|
|
14
|
+
describe('handleOpenAIError()', () => {
|
|
15
|
+
it('should convert OpenAI APIError to AIProviderError', () => {
|
|
16
|
+
const apiError = new OpenAI.APIError(
|
|
17
|
+
400,
|
|
18
|
+
{
|
|
19
|
+
error: {
|
|
20
|
+
message: 'Invalid request',
|
|
21
|
+
type: 'invalid_request_error',
|
|
22
|
+
param: 'model',
|
|
23
|
+
code: 'invalid_model'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
'Invalid request',
|
|
27
|
+
{}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const result = handleOpenAIError(apiError);
|
|
31
|
+
|
|
32
|
+
expect(result).toBeInstanceOf(AIProviderError);
|
|
33
|
+
expect(result.message).toContain('OpenAI API Error');
|
|
34
|
+
expect(result.message).toContain('Invalid request');
|
|
35
|
+
expect(result.code).toBe('INVALID_REQUEST');
|
|
36
|
+
expect(result.provider).toBe('openai');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should map 401 status to AUTHENTICATION_ERROR', () => {
|
|
40
|
+
const apiError = new OpenAI.APIError(
|
|
41
|
+
401,
|
|
42
|
+
{ error: { message: 'Invalid API key' } },
|
|
43
|
+
'Invalid API key',
|
|
44
|
+
{}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const result = handleOpenAIError(apiError);
|
|
48
|
+
|
|
49
|
+
expect(result.code).toBe('AUTHENTICATION_ERROR');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should map 403 status to PERMISSION_DENIED', () => {
|
|
53
|
+
const apiError = new OpenAI.APIError(
|
|
54
|
+
403,
|
|
55
|
+
{ error: { message: 'Permission denied' } },
|
|
56
|
+
'Permission denied',
|
|
57
|
+
{}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const result = handleOpenAIError(apiError);
|
|
61
|
+
|
|
62
|
+
expect(result.code).toBe('PERMISSION_DENIED');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should map 404 status to NOT_FOUND', () => {
|
|
66
|
+
const apiError = new OpenAI.APIError(
|
|
67
|
+
404,
|
|
68
|
+
{ error: { message: 'Model not found' } },
|
|
69
|
+
'Model not found',
|
|
70
|
+
{}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const result = handleOpenAIError(apiError);
|
|
74
|
+
|
|
75
|
+
expect(result.code).toBe('NOT_FOUND');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should map 429 status to RATE_LIMIT_ERROR', () => {
|
|
79
|
+
const apiError = new OpenAI.APIError(
|
|
80
|
+
429,
|
|
81
|
+
{ error: { message: 'Rate limit exceeded' } },
|
|
82
|
+
'Rate limit exceeded',
|
|
83
|
+
{}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const result = handleOpenAIError(apiError);
|
|
87
|
+
|
|
88
|
+
expect(result.code).toBe('RATE_LIMIT_ERROR');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should map 500 status to SERVER_ERROR', () => {
|
|
92
|
+
const apiError = new OpenAI.APIError(
|
|
93
|
+
500,
|
|
94
|
+
{ error: { message: 'Internal server error' } },
|
|
95
|
+
'Internal server error',
|
|
96
|
+
{}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const result = handleOpenAIError(apiError);
|
|
100
|
+
|
|
101
|
+
expect(result.code).toBe('SERVER_ERROR');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should map 502 status to SERVER_ERROR', () => {
|
|
105
|
+
const apiError = new OpenAI.APIError(
|
|
106
|
+
502,
|
|
107
|
+
{ error: { message: 'Bad gateway' } },
|
|
108
|
+
'Bad gateway',
|
|
109
|
+
{}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const result = handleOpenAIError(apiError);
|
|
113
|
+
|
|
114
|
+
expect(result.code).toBe('SERVER_ERROR');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should map 503 status to SERVER_ERROR', () => {
|
|
118
|
+
const apiError = new OpenAI.APIError(
|
|
119
|
+
503,
|
|
120
|
+
{ error: { message: 'Service unavailable' } },
|
|
121
|
+
'Service unavailable',
|
|
122
|
+
{}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const result = handleOpenAIError(apiError);
|
|
126
|
+
|
|
127
|
+
expect(result.code).toBe('SERVER_ERROR');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should map 504 status to TIMEOUT_ERROR', () => {
|
|
131
|
+
const apiError = new OpenAI.APIError(
|
|
132
|
+
504,
|
|
133
|
+
{ error: { message: 'Gateway timeout' } },
|
|
134
|
+
'Gateway timeout',
|
|
135
|
+
{}
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const result = handleOpenAIError(apiError);
|
|
139
|
+
|
|
140
|
+
expect(result.code).toBe('TIMEOUT_ERROR');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should map unknown status codes to UNKNOWN_ERROR', () => {
|
|
144
|
+
const apiError = new OpenAI.APIError(
|
|
145
|
+
418,
|
|
146
|
+
{ error: { message: "I'm a teapot" } },
|
|
147
|
+
"I'm a teapot",
|
|
148
|
+
{}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const result = handleOpenAIError(apiError);
|
|
152
|
+
|
|
153
|
+
expect(result.code).toBe('UNKNOWN_ERROR');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle APIError with undefined status', () => {
|
|
157
|
+
const apiError = new OpenAI.APIError(
|
|
158
|
+
undefined as any,
|
|
159
|
+
{ error: { message: 'No status' } },
|
|
160
|
+
'No status',
|
|
161
|
+
{}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const result = handleOpenAIError(apiError);
|
|
165
|
+
|
|
166
|
+
expect(result.code).toBe('UNKNOWN_ERROR');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle regular Error objects', () => {
|
|
170
|
+
const error = new Error('Something went wrong');
|
|
171
|
+
|
|
172
|
+
const result = handleOpenAIError(error);
|
|
173
|
+
|
|
174
|
+
expect(result).toBeInstanceOf(AIProviderError);
|
|
175
|
+
expect(result.message).toBe('Something went wrong');
|
|
176
|
+
expect(result.code).toBe('UNKNOWN_ERROR');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle unknown error types', () => {
|
|
180
|
+
const result = handleOpenAIError('string error');
|
|
181
|
+
|
|
182
|
+
expect(result).toBeInstanceOf(AIProviderError);
|
|
183
|
+
expect(result.message).toBe('Unknown error occurred');
|
|
184
|
+
expect(result.code).toBe('UNKNOWN_ERROR');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should include error details in AIProviderError', () => {
|
|
188
|
+
const apiError = new OpenAI.APIError(
|
|
189
|
+
400,
|
|
190
|
+
{
|
|
191
|
+
error: {
|
|
192
|
+
message: 'Invalid request',
|
|
193
|
+
type: 'invalid_request_error',
|
|
194
|
+
param: 'model',
|
|
195
|
+
code: 'invalid_model'
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
'Invalid request',
|
|
199
|
+
{}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = handleOpenAIError(apiError);
|
|
203
|
+
|
|
204
|
+
expect(result.details).toBeDefined();
|
|
205
|
+
expect(result.details?.status).toBe(400);
|
|
206
|
+
// OpenAI.APIError type and code properties may not be available on all versions
|
|
207
|
+
// So we just verify the details object exists
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('isRetryableError()', () => {
|
|
212
|
+
it('should identify RATE_LIMIT_ERROR as retryable', () => {
|
|
213
|
+
const error = new AIProviderError('Rate limit', 'RATE_LIMIT_ERROR', 'openai');
|
|
214
|
+
expect(isRetryableError(error)).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should identify SERVER_ERROR as retryable', () => {
|
|
218
|
+
const error = new AIProviderError('Server error', 'SERVER_ERROR', 'openai');
|
|
219
|
+
expect(isRetryableError(error)).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should identify TIMEOUT_ERROR as retryable', () => {
|
|
223
|
+
const error = new AIProviderError('Timeout', 'TIMEOUT_ERROR', 'openai');
|
|
224
|
+
expect(isRetryableError(error)).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should identify NETWORK_ERROR as retryable', () => {
|
|
228
|
+
const error = new AIProviderError('Network error', 'NETWORK_ERROR', 'openai');
|
|
229
|
+
expect(isRetryableError(error)).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should identify AUTHENTICATION_ERROR as not retryable', () => {
|
|
233
|
+
const error = new AIProviderError('Auth error', 'AUTHENTICATION_ERROR', 'openai');
|
|
234
|
+
expect(isRetryableError(error)).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should identify INVALID_REQUEST as not retryable', () => {
|
|
238
|
+
const error = new AIProviderError('Invalid', 'INVALID_REQUEST', 'openai');
|
|
239
|
+
expect(isRetryableError(error)).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should identify PERMISSION_DENIED as not retryable', () => {
|
|
243
|
+
const error = new AIProviderError('Denied', 'PERMISSION_DENIED', 'openai');
|
|
244
|
+
expect(isRetryableError(error)).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('getRetryDelay()', () => {
|
|
249
|
+
it('should return exponential backoff for rate limit errors', () => {
|
|
250
|
+
const error = new AIProviderError('Rate limit', 'RATE_LIMIT_ERROR', 'openai');
|
|
251
|
+
|
|
252
|
+
const delay1 = getRetryDelay(error, 1);
|
|
253
|
+
const delay2 = getRetryDelay(error, 2);
|
|
254
|
+
const delay3 = getRetryDelay(error, 3);
|
|
255
|
+
|
|
256
|
+
expect(delay1).toBe(2000); // 2^1 * 1000
|
|
257
|
+
expect(delay2).toBe(4000); // 2^2 * 1000
|
|
258
|
+
expect(delay3).toBe(8000); // 2^3 * 1000
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should return exponential backoff for timeout errors', () => {
|
|
262
|
+
const error = new AIProviderError('Timeout', 'TIMEOUT_ERROR', 'openai');
|
|
263
|
+
|
|
264
|
+
const delay1 = getRetryDelay(error, 1);
|
|
265
|
+
const delay2 = getRetryDelay(error, 2);
|
|
266
|
+
|
|
267
|
+
expect(delay1).toBe(1000); // 2^1 * 500
|
|
268
|
+
expect(delay2).toBe(2000); // 2^2 * 500
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should return exponential backoff for network errors', () => {
|
|
272
|
+
const error = new AIProviderError('Network', 'NETWORK_ERROR', 'openai');
|
|
273
|
+
|
|
274
|
+
const delay = getRetryDelay(error, 2);
|
|
275
|
+
|
|
276
|
+
expect(delay).toBe(2000); // 2^2 * 500
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should cap rate limit retry delay at 60 seconds', () => {
|
|
280
|
+
const error = new AIProviderError('Rate limit', 'RATE_LIMIT_ERROR', 'openai');
|
|
281
|
+
|
|
282
|
+
const delay = getRetryDelay(error, 10);
|
|
283
|
+
|
|
284
|
+
expect(delay).toBe(60000);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should return exponential backoff for server errors', () => {
|
|
288
|
+
const error = new AIProviderError('Server error', 'SERVER_ERROR', 'openai');
|
|
289
|
+
|
|
290
|
+
const delay1 = getRetryDelay(error, 1);
|
|
291
|
+
const delay2 = getRetryDelay(error, 2);
|
|
292
|
+
const delay3 = getRetryDelay(error, 3);
|
|
293
|
+
|
|
294
|
+
expect(delay1).toBe(1000); // 2^1 * 500
|
|
295
|
+
expect(delay2).toBe(2000); // 2^2 * 500
|
|
296
|
+
expect(delay3).toBe(4000); // 2^3 * 500
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should cap other retryable error delays at 30 seconds', () => {
|
|
300
|
+
const error = new AIProviderError('Timeout', 'TIMEOUT_ERROR', 'openai');
|
|
301
|
+
|
|
302
|
+
const delay = getRetryDelay(error, 10);
|
|
303
|
+
|
|
304
|
+
expect(delay).toBe(30000);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should return 0 for non-retryable errors', () => {
|
|
308
|
+
const error = new AIProviderError('Invalid', 'INVALID_REQUEST', 'openai');
|
|
309
|
+
|
|
310
|
+
const delay = getRetryDelay(error, 1);
|
|
311
|
+
|
|
312
|
+
expect(delay).toBe(0);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code only within the scope of the project it was delivered for.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
OPENAI_MODELS,
|
|
11
|
+
getModelInfo,
|
|
12
|
+
getChatModels,
|
|
13
|
+
getEmbeddingModels,
|
|
14
|
+
getVisionModels,
|
|
15
|
+
supportsFunctionCalling
|
|
16
|
+
} from '../src/models/model-registry';
|
|
17
|
+
|
|
18
|
+
describe('Model Registry', () => {
|
|
19
|
+
describe('OPENAI_MODELS', () => {
|
|
20
|
+
it('should contain core chat models', () => {
|
|
21
|
+
const modelIds = OPENAI_MODELS.map(m => m.id);
|
|
22
|
+
|
|
23
|
+
expect(modelIds).toContain('gpt-4-turbo');
|
|
24
|
+
expect(modelIds).toContain('gpt-4');
|
|
25
|
+
expect(modelIds).toContain('gpt-3.5-turbo');
|
|
26
|
+
expect(modelIds).toContain('gpt-4-vision-preview');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should contain embedding models', () => {
|
|
30
|
+
const modelIds = OPENAI_MODELS.map(m => m.id);
|
|
31
|
+
|
|
32
|
+
expect(modelIds).toContain('text-embedding-3-small');
|
|
33
|
+
expect(modelIds).toContain('text-embedding-3-large');
|
|
34
|
+
expect(modelIds).toContain('text-embedding-ada-002');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should have correct pricing for gpt-4-turbo', () => {
|
|
38
|
+
const model = OPENAI_MODELS.find(m => m.id === 'gpt-4-turbo');
|
|
39
|
+
|
|
40
|
+
expect(model).toBeDefined();
|
|
41
|
+
expect(model!.pricing).toBeDefined();
|
|
42
|
+
expect(model!.pricing!.inputPricePerToken).toBe(0.01 / 1000);
|
|
43
|
+
expect(model!.pricing!.outputPricePerToken).toBe(0.03 / 1000);
|
|
44
|
+
expect(model!.pricing!.currency).toBe('USD');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should have correct pricing for gpt-3.5-turbo', () => {
|
|
48
|
+
const model = OPENAI_MODELS.find(m => m.id === 'gpt-3.5-turbo');
|
|
49
|
+
|
|
50
|
+
expect(model).toBeDefined();
|
|
51
|
+
expect(model!.pricing).toBeDefined();
|
|
52
|
+
expect(model!.pricing!.inputPricePerToken).toBe(0.0005 / 1000);
|
|
53
|
+
expect(model!.pricing!.outputPricePerToken).toBe(0.0015 / 1000);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should have correct context windows', () => {
|
|
57
|
+
const gpt4Turbo = OPENAI_MODELS.find(m => m.id === 'gpt-4-turbo');
|
|
58
|
+
const gpt4 = OPENAI_MODELS.find(m => m.id === 'gpt-4');
|
|
59
|
+
const gpt35 = OPENAI_MODELS.find(m => m.id === 'gpt-3.5-turbo');
|
|
60
|
+
|
|
61
|
+
expect(gpt4Turbo!.contextWindow).toBe(128000);
|
|
62
|
+
expect(gpt4!.contextWindow).toBe(8192);
|
|
63
|
+
expect(gpt35!.contextWindow).toBe(16385);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should have correct capabilities for chat models', () => {
|
|
67
|
+
const gpt4Turbo = OPENAI_MODELS.find(m => m.id === 'gpt-4-turbo');
|
|
68
|
+
|
|
69
|
+
expect(gpt4Turbo!.capabilities).toContain('chat');
|
|
70
|
+
expect(gpt4Turbo!.capabilities).toContain('completion');
|
|
71
|
+
expect(gpt4Turbo!.capabilities).toContain('function-calling');
|
|
72
|
+
expect(gpt4Turbo!.capabilities).toContain('vision');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should have correct capabilities for embedding models', () => {
|
|
76
|
+
const embedding = OPENAI_MODELS.find(m => m.id === 'text-embedding-3-small');
|
|
77
|
+
|
|
78
|
+
expect(embedding!.capabilities).toContain('embeddings');
|
|
79
|
+
expect(embedding!.capabilities).not.toContain('chat');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('getModelInfo()', () => {
|
|
84
|
+
it('should return model info for valid model ID', () => {
|
|
85
|
+
const model = getModelInfo('gpt-4-turbo');
|
|
86
|
+
|
|
87
|
+
expect(model).toBeDefined();
|
|
88
|
+
expect(model!.id).toBe('gpt-4-turbo');
|
|
89
|
+
expect(model!.name).toBe('GPT-4 Turbo');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return undefined for unknown model ID', () => {
|
|
93
|
+
const model = getModelInfo('unknown-model');
|
|
94
|
+
|
|
95
|
+
expect(model).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return embedding model info', () => {
|
|
99
|
+
const model = getModelInfo('text-embedding-3-small');
|
|
100
|
+
|
|
101
|
+
expect(model).toBeDefined();
|
|
102
|
+
expect(model!.capabilities).toContain('embeddings');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('getChatModels()', () => {
|
|
107
|
+
it('should return all chat-capable models', () => {
|
|
108
|
+
const chatModels = getChatModels();
|
|
109
|
+
|
|
110
|
+
expect(chatModels.length).toBeGreaterThan(0);
|
|
111
|
+
|
|
112
|
+
chatModels.forEach(model => {
|
|
113
|
+
const hasChat = model.capabilities.includes('chat') ||
|
|
114
|
+
model.capabilities.includes('completion');
|
|
115
|
+
expect(hasChat).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should include GPT-4 models', () => {
|
|
120
|
+
const chatModels = getChatModels();
|
|
121
|
+
const modelIds = chatModels.map(m => m.id);
|
|
122
|
+
|
|
123
|
+
expect(modelIds).toContain('gpt-4-turbo');
|
|
124
|
+
expect(modelIds).toContain('gpt-4');
|
|
125
|
+
expect(modelIds).toContain('gpt-3.5-turbo');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should not include embedding models', () => {
|
|
129
|
+
const chatModels = getChatModels();
|
|
130
|
+
const hasEmbeddingOnly = chatModels.some(m =>
|
|
131
|
+
m.capabilities.includes('embeddings') &&
|
|
132
|
+
!m.capabilities.includes('chat') &&
|
|
133
|
+
!m.capabilities.includes('completion')
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(hasEmbeddingOnly).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('getEmbeddingModels()', () => {
|
|
141
|
+
it('should return all embedding models', () => {
|
|
142
|
+
const embeddingModels = getEmbeddingModels();
|
|
143
|
+
|
|
144
|
+
expect(embeddingModels.length).toBeGreaterThan(0);
|
|
145
|
+
|
|
146
|
+
embeddingModels.forEach(model => {
|
|
147
|
+
expect(model.capabilities).toContain('embeddings');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should include all embedding model variants', () => {
|
|
152
|
+
const embeddingModels = getEmbeddingModels();
|
|
153
|
+
const modelIds = embeddingModels.map(m => m.id);
|
|
154
|
+
|
|
155
|
+
expect(modelIds).toContain('text-embedding-3-small');
|
|
156
|
+
expect(modelIds).toContain('text-embedding-3-large');
|
|
157
|
+
expect(modelIds).toContain('text-embedding-ada-002');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should not include chat models', () => {
|
|
161
|
+
const embeddingModels = getEmbeddingModels();
|
|
162
|
+
const hasChatOnly = embeddingModels.some(m =>
|
|
163
|
+
(m.capabilities.includes('chat') || m.capabilities.includes('completion')) &&
|
|
164
|
+
!m.capabilities.includes('embeddings')
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(hasChatOnly).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('getVisionModels()', () => {
|
|
172
|
+
it('should return all vision-capable models', () => {
|
|
173
|
+
const visionModels = getVisionModels();
|
|
174
|
+
|
|
175
|
+
expect(visionModels.length).toBeGreaterThan(0);
|
|
176
|
+
|
|
177
|
+
visionModels.forEach(model => {
|
|
178
|
+
expect(model.capabilities).toContain('vision');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should include GPT-4 Vision models', () => {
|
|
183
|
+
const visionModels = getVisionModels();
|
|
184
|
+
const modelIds = visionModels.map(m => m.id);
|
|
185
|
+
|
|
186
|
+
expect(modelIds).toContain('gpt-4-vision-preview');
|
|
187
|
+
expect(modelIds).toContain('gpt-4-turbo');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should not include non-vision models', () => {
|
|
191
|
+
const visionModels = getVisionModels();
|
|
192
|
+
const modelIds = visionModels.map(m => m.id);
|
|
193
|
+
|
|
194
|
+
expect(modelIds).not.toContain('gpt-3.5-turbo');
|
|
195
|
+
expect(modelIds).not.toContain('text-embedding-3-small');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('supportsFunctionCalling()', () => {
|
|
200
|
+
it('should return true for models with function calling capability', () => {
|
|
201
|
+
expect(supportsFunctionCalling('gpt-4-turbo')).toBe(true);
|
|
202
|
+
expect(supportsFunctionCalling('gpt-4')).toBe(true);
|
|
203
|
+
expect(supportsFunctionCalling('gpt-3.5-turbo')).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should return false for embedding models', () => {
|
|
207
|
+
expect(supportsFunctionCalling('text-embedding-3-small')).toBe(false);
|
|
208
|
+
expect(supportsFunctionCalling('text-embedding-3-large')).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should return false for unknown models', () => {
|
|
212
|
+
expect(supportsFunctionCalling('unknown-model')).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should return false for vision-only models without function calling', () => {
|
|
216
|
+
const visionModel = OPENAI_MODELS.find(m =>
|
|
217
|
+
m.capabilities.includes('vision') &&
|
|
218
|
+
!m.capabilities.includes('function-calling')
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (visionModel) {
|
|
222
|
+
expect(supportsFunctionCalling(visionModel.id)).toBe(false);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('Model Completeness', () => {
|
|
228
|
+
it('should have all required fields for each model', () => {
|
|
229
|
+
OPENAI_MODELS.forEach(model => {
|
|
230
|
+
expect(model.id).toBeDefined();
|
|
231
|
+
expect(model.name).toBeDefined();
|
|
232
|
+
expect(model.contextWindow).toBeGreaterThan(0);
|
|
233
|
+
expect(model.maxOutputTokens).toBeGreaterThanOrEqual(0);
|
|
234
|
+
expect(model.capabilities).toBeDefined();
|
|
235
|
+
expect(model.capabilities.length).toBeGreaterThan(0);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should have pricing for all models', () => {
|
|
240
|
+
OPENAI_MODELS.forEach(model => {
|
|
241
|
+
if (model.capabilities.includes('chat') ||
|
|
242
|
+
model.capabilities.includes('completion') ||
|
|
243
|
+
model.capabilities.includes('embeddings')) {
|
|
244
|
+
expect(model.pricing).toBeDefined();
|
|
245
|
+
expect(model.pricing!.inputPricePerToken).toBeGreaterThan(0);
|
|
246
|
+
expect(model.pricing!.currency).toBe('USD');
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should have valid capability combinations', () => {
|
|
252
|
+
OPENAI_MODELS.forEach(model => {
|
|
253
|
+
const capabilities = model.capabilities;
|
|
254
|
+
|
|
255
|
+
// Embedding models shouldn't have chat/completion
|
|
256
|
+
if (capabilities.includes('embeddings') && capabilities.length === 1) {
|
|
257
|
+
expect(capabilities).not.toContain('chat');
|
|
258
|
+
expect(capabilities).not.toContain('completion');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Vision models should also have chat/completion
|
|
262
|
+
if (capabilities.includes('vision')) {
|
|
263
|
+
const hasChatOrCompletion = capabilities.includes('chat') ||
|
|
264
|
+
capabilities.includes('completion');
|
|
265
|
+
expect(hasChatOrCompletion).toBe(true);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|