@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,333 @@
|
|
|
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 { OpenAIProvider } from '../src/OpenAIProvider';
|
|
10
|
+
import { OpenAIProviderConfig, OpenAIFunction } from '../src/types/openai-types';
|
|
11
|
+
import OpenAI from 'openai';
|
|
12
|
+
|
|
13
|
+
jest.mock('openai');
|
|
14
|
+
|
|
15
|
+
describe('OpenAI-Specific Features', () => {
|
|
16
|
+
let provider: OpenAIProvider;
|
|
17
|
+
let mockClient: jest.Mocked<OpenAI>;
|
|
18
|
+
|
|
19
|
+
const config: OpenAIProviderConfig = {
|
|
20
|
+
providerName: 'openai',
|
|
21
|
+
apiKey: 'test-api-key',
|
|
22
|
+
defaultModel: 'gpt-4-turbo'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
|
|
28
|
+
mockClient = {
|
|
29
|
+
chat: {
|
|
30
|
+
completions: {
|
|
31
|
+
create: jest.fn() as any
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} as any;
|
|
35
|
+
|
|
36
|
+
(OpenAI as jest.MockedClass<typeof OpenAI>).mockImplementation(() => mockClient);
|
|
37
|
+
|
|
38
|
+
provider = new OpenAIProvider(config);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('completionWithFunctions()', () => {
|
|
42
|
+
it('should call OpenAI with function definitions', async () => {
|
|
43
|
+
const functions: OpenAIFunction[] = [
|
|
44
|
+
{
|
|
45
|
+
name: 'get_weather',
|
|
46
|
+
description: 'Get current weather',
|
|
47
|
+
parameters: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
location: { type: 'string', description: 'City name' },
|
|
51
|
+
unit: { type: 'string', enum: ['celsius', 'fahrenheit'] }
|
|
52
|
+
},
|
|
53
|
+
required: ['location']
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const mockResponse = {
|
|
59
|
+
id: 'chatcmpl-123',
|
|
60
|
+
model: 'gpt-4-turbo',
|
|
61
|
+
created: Date.now(),
|
|
62
|
+
choices: [
|
|
63
|
+
{
|
|
64
|
+
message: {
|
|
65
|
+
role: 'assistant',
|
|
66
|
+
content: null,
|
|
67
|
+
function_call: {
|
|
68
|
+
name: 'get_weather',
|
|
69
|
+
arguments: '{"location":"San Francisco","unit":"celsius"}'
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
finish_reason: 'function_call'
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
usage: {
|
|
76
|
+
prompt_tokens: 50,
|
|
77
|
+
completion_tokens: 20,
|
|
78
|
+
total_tokens: 70
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
mockClient.chat.completions.create.mockResolvedValue(mockResponse as any);
|
|
83
|
+
|
|
84
|
+
const result = await provider.completionWithFunctions({
|
|
85
|
+
messages: [{ role: 'user', content: 'What is the weather in San Francisco?' }],
|
|
86
|
+
functions
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.success).toBe(true);
|
|
90
|
+
expect(result.metadata?.functionCall).toEqual({
|
|
91
|
+
name: 'get_weather',
|
|
92
|
+
arguments: '{"location":"San Francisco","unit":"celsius"}'
|
|
93
|
+
});
|
|
94
|
+
expect(result.finishReason).toBe('function_call');
|
|
95
|
+
|
|
96
|
+
expect(mockClient.chat.completions.create).toHaveBeenCalledWith({
|
|
97
|
+
model: 'gpt-4-turbo',
|
|
98
|
+
messages: [{ role: 'user', content: 'What is the weather in San Francisco?' }],
|
|
99
|
+
functions,
|
|
100
|
+
function_call: 'auto'
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle regular text response when function not needed', async () => {
|
|
105
|
+
const functions: OpenAIFunction[] = [
|
|
106
|
+
{
|
|
107
|
+
name: 'get_weather',
|
|
108
|
+
description: 'Get current weather',
|
|
109
|
+
parameters: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: { location: { type: 'string' } },
|
|
112
|
+
required: ['location']
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const mockResponse = {
|
|
118
|
+
choices: [
|
|
119
|
+
{
|
|
120
|
+
message: {
|
|
121
|
+
role: 'assistant',
|
|
122
|
+
content: 'I need more information to check the weather.',
|
|
123
|
+
function_call: undefined
|
|
124
|
+
},
|
|
125
|
+
finish_reason: 'stop'
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
usage: {
|
|
129
|
+
prompt_tokens: 30,
|
|
130
|
+
completion_tokens: 10,
|
|
131
|
+
total_tokens: 40
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
mockClient.chat.completions.create.mockResolvedValue(mockResponse as any);
|
|
136
|
+
|
|
137
|
+
const result = await provider.completionWithFunctions({
|
|
138
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
139
|
+
functions
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.success).toBe(true);
|
|
143
|
+
expect(result.content).toBe('I need more information to check the weather.');
|
|
144
|
+
expect(result.finishReason).toBe('stop');
|
|
145
|
+
expect(result.metadata?.functionCall).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle multiple function definitions', async () => {
|
|
149
|
+
const functions: OpenAIFunction[] = [
|
|
150
|
+
{
|
|
151
|
+
name: 'get_weather',
|
|
152
|
+
description: 'Get weather',
|
|
153
|
+
parameters: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: { location: { type: 'string' } },
|
|
156
|
+
required: ['location']
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'get_time',
|
|
161
|
+
description: 'Get current time',
|
|
162
|
+
parameters: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: { timezone: { type: 'string' } }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
mockClient.chat.completions.create.mockResolvedValue({
|
|
170
|
+
choices: [
|
|
171
|
+
{
|
|
172
|
+
message: {
|
|
173
|
+
role: 'assistant',
|
|
174
|
+
content: null,
|
|
175
|
+
function_call: { name: 'get_time', arguments: '{"timezone":"PST"}' }
|
|
176
|
+
},
|
|
177
|
+
finish_reason: 'function_call'
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
} as any);
|
|
181
|
+
|
|
182
|
+
const result = await provider.completionWithFunctions({
|
|
183
|
+
messages: [{ role: 'user', content: 'What time is it in PST?' }],
|
|
184
|
+
functions
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(result.success).toBe(true);
|
|
188
|
+
expect(result.metadata?.functionCall?.name).toBe('get_time');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle API errors', async () => {
|
|
192
|
+
const functions: OpenAIFunction[] = [
|
|
193
|
+
{
|
|
194
|
+
name: 'test_function',
|
|
195
|
+
description: 'Test',
|
|
196
|
+
parameters: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
mockClient.chat.completions.create.mockRejectedValue(new Error('API Error'));
|
|
204
|
+
|
|
205
|
+
const result = await provider.completionWithFunctions({
|
|
206
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
207
|
+
functions
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(result.success).toBe(false);
|
|
211
|
+
expect(result.error).toBeDefined();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('analyzeImage()', () => {
|
|
216
|
+
it('should analyze image with GPT-4 Vision', async () => {
|
|
217
|
+
const mockResponse = {
|
|
218
|
+
model: 'gpt-4-vision-preview',
|
|
219
|
+
choices: [
|
|
220
|
+
{
|
|
221
|
+
message: {
|
|
222
|
+
role: 'assistant',
|
|
223
|
+
content: 'The image shows a beautiful sunset over the ocean with orange and purple hues.'
|
|
224
|
+
},
|
|
225
|
+
finish_reason: 'stop'
|
|
226
|
+
}
|
|
227
|
+
],
|
|
228
|
+
usage: {
|
|
229
|
+
prompt_tokens: 100,
|
|
230
|
+
completion_tokens: 50,
|
|
231
|
+
total_tokens: 150
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
mockClient.chat.completions.create.mockResolvedValue(mockResponse as any);
|
|
236
|
+
|
|
237
|
+
const result = await provider.analyzeImage(
|
|
238
|
+
'https://example.com/sunset.jpg',
|
|
239
|
+
'Describe this image in detail'
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(result.success).toBe(true);
|
|
243
|
+
expect(result.content).toContain('sunset');
|
|
244
|
+
expect(result.usage?.totalTokens).toBe(150);
|
|
245
|
+
expect(result.model).toBe('gpt-4-vision-preview');
|
|
246
|
+
|
|
247
|
+
expect(mockClient.chat.completions.create).toHaveBeenCalledWith({
|
|
248
|
+
model: 'gpt-4-vision-preview',
|
|
249
|
+
messages: [
|
|
250
|
+
{
|
|
251
|
+
role: 'user',
|
|
252
|
+
content: [
|
|
253
|
+
{ type: 'text', text: 'Describe this image in detail' },
|
|
254
|
+
{ type: 'image_url', image_url: { url: 'https://example.com/sunset.jpg' } }
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
max_tokens: 1000
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should support custom model', async () => {
|
|
263
|
+
mockClient.chat.completions.create.mockResolvedValue({
|
|
264
|
+
model: 'gpt-4-turbo',
|
|
265
|
+
choices: [{ message: { content: 'Analysis result' }, finish_reason: 'stop' }]
|
|
266
|
+
} as any);
|
|
267
|
+
|
|
268
|
+
await provider.analyzeImage(
|
|
269
|
+
'https://example.com/image.jpg',
|
|
270
|
+
'Analyze this',
|
|
271
|
+
'gpt-4-turbo'
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(mockClient.chat.completions.create).toHaveBeenCalledWith(
|
|
275
|
+
expect.objectContaining({
|
|
276
|
+
model: 'gpt-4-turbo'
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should handle empty or null content', async () => {
|
|
282
|
+
mockClient.chat.completions.create.mockResolvedValue({
|
|
283
|
+
choices: [{ message: { content: null }, finish_reason: 'stop' }]
|
|
284
|
+
} as any);
|
|
285
|
+
|
|
286
|
+
const result = await provider.analyzeImage(
|
|
287
|
+
'https://example.com/image.jpg',
|
|
288
|
+
'Describe this'
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(result.success).toBe(true);
|
|
292
|
+
expect(result.content).toBe('');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should handle API errors', async () => {
|
|
296
|
+
const apiError = new OpenAI.APIError(
|
|
297
|
+
400,
|
|
298
|
+
{ error: { message: 'Invalid image URL' } },
|
|
299
|
+
'Invalid image URL',
|
|
300
|
+
{}
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
mockClient.chat.completions.create.mockRejectedValue(apiError);
|
|
304
|
+
|
|
305
|
+
const result = await provider.analyzeImage(
|
|
306
|
+
'invalid-url',
|
|
307
|
+
'Describe this'
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
expect(result.success).toBe(false);
|
|
311
|
+
expect(result.error).toContain('OpenAI API Error');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should handle rate limiting', async () => {
|
|
315
|
+
const rateLimitError = new OpenAI.APIError(
|
|
316
|
+
429,
|
|
317
|
+
{ error: { message: 'Rate limit exceeded' } },
|
|
318
|
+
'Rate limit exceeded',
|
|
319
|
+
{}
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
mockClient.chat.completions.create.mockRejectedValue(rateLimitError);
|
|
323
|
+
|
|
324
|
+
const result = await provider.analyzeImage(
|
|
325
|
+
'https://example.com/image.jpg',
|
|
326
|
+
'Describe'
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
expect(result.success).toBe(false);
|
|
330
|
+
expect(result.error).toBeDefined();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AIProvider, CompletionRequest, CompletionResponse, StreamChunk, EmbeddingRequest, EmbeddingResponse, ModerationResponse, ModelInfo, HealthStatus, CostEstimate } from '@bernierllc/ai-provider-core';
|
|
2
|
+
import { OpenAIProviderConfig, OpenAIFunction } from './types/openai-types';
|
|
3
|
+
export declare class OpenAIProvider extends AIProvider {
|
|
4
|
+
private client;
|
|
5
|
+
private models;
|
|
6
|
+
constructor(config: OpenAIProviderConfig);
|
|
7
|
+
complete(request: CompletionRequest): Promise<CompletionResponse>;
|
|
8
|
+
streamComplete(request: CompletionRequest): AsyncGenerator<StreamChunk, void, unknown>;
|
|
9
|
+
generateEmbeddings(request: EmbeddingRequest): Promise<EmbeddingResponse>;
|
|
10
|
+
moderate(content: string): Promise<ModerationResponse>;
|
|
11
|
+
getAvailableModels(): Promise<ModelInfo[]>;
|
|
12
|
+
checkHealth(): Promise<HealthStatus>;
|
|
13
|
+
completionWithFunctions(request: CompletionRequest & {
|
|
14
|
+
functions: OpenAIFunction[];
|
|
15
|
+
}): Promise<CompletionResponse>;
|
|
16
|
+
analyzeImage(imageUrl: string, prompt: string, model?: string): Promise<CompletionResponse>;
|
|
17
|
+
estimateCost(request: CompletionRequest): CostEstimate;
|
|
18
|
+
private initializeModels;
|
|
19
|
+
private mapOpenAIModel;
|
|
20
|
+
private getModelPricing;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=OpenAIProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OpenAIProvider.d.ts","sourceRoot":"","sources":["../src/OpenAIProvider.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,SAAS,EACT,YAAY,EACZ,YAAY,EACb,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAQ5E,qBAAa,cAAe,SAAQ,UAAU;IAC5C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAqC;gBAEvC,MAAM,EAAE,oBAAoB;IAqBlC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA8DhE,cAAc,CACnB,OAAO,EAAE,iBAAiB,GACzB,cAAc,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,CAAC;IA6CvC,kBAAkB,CACtB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,iBAAiB,CAAC;IA8BvB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA2BtD,kBAAkB,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAmB1C,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC;IA+BpC,uBAAuB,CAC3B,OAAO,EAAE,iBAAiB,GAAG;QAAE,SAAS,EAAE,cAAc,EAAE,CAAA;KAAE,GAC3D,OAAO,CAAC,kBAAkB,CAAC;IA6CxB,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAA+B,GACrC,OAAO,CAAC,kBAAkB,CAAC;IAkD9B,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,YAAY;IAyBtD,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,cAAc;IAetB,OAAO,CAAC,eAAe;CAexB"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OpenAIProvider = void 0;
|
|
7
|
+
const ai_provider_core_1 = require("@bernierllc/ai-provider-core");
|
|
8
|
+
const openai_1 = __importDefault(require("openai"));
|
|
9
|
+
const model_registry_1 = require("./models/model-registry");
|
|
10
|
+
const error_handling_1 = require("./utils/error-handling");
|
|
11
|
+
class OpenAIProvider extends ai_provider_core_1.AIProvider {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
super(config);
|
|
14
|
+
this.models = new Map();
|
|
15
|
+
this.client = new openai_1.default({
|
|
16
|
+
apiKey: config.apiKey,
|
|
17
|
+
organization: config.organizationId,
|
|
18
|
+
baseURL: config.baseURL,
|
|
19
|
+
timeout: config.timeout || 60000,
|
|
20
|
+
maxRetries: config.maxRetries || 3
|
|
21
|
+
});
|
|
22
|
+
this.initializeModels();
|
|
23
|
+
}
|
|
24
|
+
async complete(request) {
|
|
25
|
+
const validation = this.validateRequest(request);
|
|
26
|
+
if (!validation.isValid) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
error: validation.errors.join(', ')
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const completion = await this.client.chat.completions.create({
|
|
34
|
+
model: request.model || this.config.defaultModel || 'gpt-4-turbo',
|
|
35
|
+
messages: request.messages.map(msg => ({
|
|
36
|
+
role: msg.role,
|
|
37
|
+
content: msg.content,
|
|
38
|
+
name: msg.name
|
|
39
|
+
})),
|
|
40
|
+
max_tokens: request.maxTokens,
|
|
41
|
+
temperature: request.temperature,
|
|
42
|
+
top_p: request.topP,
|
|
43
|
+
frequency_penalty: request.frequencyPenalty,
|
|
44
|
+
presence_penalty: request.presencePenalty,
|
|
45
|
+
stop: request.stop,
|
|
46
|
+
user: request.user
|
|
47
|
+
});
|
|
48
|
+
const choice = completion.choices[0];
|
|
49
|
+
const finishReason = choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call'
|
|
50
|
+
? 'function_call'
|
|
51
|
+
: choice.finish_reason;
|
|
52
|
+
return {
|
|
53
|
+
success: true,
|
|
54
|
+
content: choice.message.content || '',
|
|
55
|
+
finishReason,
|
|
56
|
+
usage: completion.usage ? {
|
|
57
|
+
promptTokens: completion.usage.prompt_tokens,
|
|
58
|
+
completionTokens: completion.usage.completion_tokens,
|
|
59
|
+
totalTokens: completion.usage.total_tokens
|
|
60
|
+
} : undefined,
|
|
61
|
+
model: completion.model,
|
|
62
|
+
metadata: {
|
|
63
|
+
id: completion.id,
|
|
64
|
+
created: completion.created,
|
|
65
|
+
systemFingerprint: completion.system_fingerprint
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const aiError = (0, error_handling_1.handleOpenAIError)(error);
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: aiError.message
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async *streamComplete(request) {
|
|
78
|
+
const validation = this.validateRequest(request);
|
|
79
|
+
if (!validation.isValid) {
|
|
80
|
+
throw new Error(validation.errors.join(', '));
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const stream = await this.client.chat.completions.create({
|
|
84
|
+
model: request.model || this.config.defaultModel || 'gpt-4-turbo',
|
|
85
|
+
messages: request.messages.map(msg => ({
|
|
86
|
+
role: msg.role,
|
|
87
|
+
content: msg.content
|
|
88
|
+
})),
|
|
89
|
+
max_tokens: request.maxTokens,
|
|
90
|
+
temperature: request.temperature,
|
|
91
|
+
stream: true
|
|
92
|
+
});
|
|
93
|
+
for await (const chunk of stream) {
|
|
94
|
+
const delta = chunk.choices[0]?.delta?.content || '';
|
|
95
|
+
const rawFinishReason = chunk.choices[0]?.finish_reason;
|
|
96
|
+
const finishReason = (rawFinishReason === 'stop' || rawFinishReason === 'length' || rawFinishReason === 'content_filter')
|
|
97
|
+
? rawFinishReason
|
|
98
|
+
: undefined;
|
|
99
|
+
yield {
|
|
100
|
+
delta,
|
|
101
|
+
finishReason,
|
|
102
|
+
usage: chunk.usage ? {
|
|
103
|
+
promptTokens: chunk.usage.prompt_tokens,
|
|
104
|
+
completionTokens: chunk.usage.completion_tokens,
|
|
105
|
+
totalTokens: chunk.usage.total_tokens
|
|
106
|
+
} : undefined
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw (0, error_handling_1.handleOpenAIError)(error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async generateEmbeddings(request) {
|
|
115
|
+
try {
|
|
116
|
+
const response = await this.client.embeddings.create({
|
|
117
|
+
model: request.model || 'text-embedding-3-small',
|
|
118
|
+
input: request.input,
|
|
119
|
+
user: request.user
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
embeddings: response.data.map(item => item.embedding),
|
|
124
|
+
usage: {
|
|
125
|
+
promptTokens: response.usage.prompt_tokens,
|
|
126
|
+
completionTokens: 0,
|
|
127
|
+
totalTokens: response.usage.total_tokens
|
|
128
|
+
},
|
|
129
|
+
model: response.model
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const aiError = (0, error_handling_1.handleOpenAIError)(error);
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: aiError.message
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async moderate(content) {
|
|
141
|
+
try {
|
|
142
|
+
const response = await this.client.moderations.create({
|
|
143
|
+
input: content
|
|
144
|
+
});
|
|
145
|
+
const result = response.results[0];
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
flagged: result.flagged,
|
|
149
|
+
categories: result.categories,
|
|
150
|
+
categoryScores: result.category_scores
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const aiError = (0, error_handling_1.handleOpenAIError)(error);
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
flagged: false,
|
|
158
|
+
error: aiError.message
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async getAvailableModels() {
|
|
163
|
+
try {
|
|
164
|
+
const response = await this.client.models.list();
|
|
165
|
+
return response.data
|
|
166
|
+
.filter(model => model.id.startsWith('gpt-') ||
|
|
167
|
+
model.id.startsWith('text-embedding'))
|
|
168
|
+
.map(model => this.mapOpenAIModel(model));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return Array.from(this.models.values());
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async checkHealth() {
|
|
175
|
+
const startTime = Date.now();
|
|
176
|
+
try {
|
|
177
|
+
await this.client.models.retrieve('gpt-3.5-turbo');
|
|
178
|
+
return {
|
|
179
|
+
status: 'healthy',
|
|
180
|
+
latency: Date.now() - startTime,
|
|
181
|
+
lastChecked: new Date()
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
status: 'unavailable',
|
|
187
|
+
latency: Date.now() - startTime,
|
|
188
|
+
lastChecked: new Date(),
|
|
189
|
+
details: {
|
|
190
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async completionWithFunctions(request) {
|
|
196
|
+
try {
|
|
197
|
+
const completion = await this.client.chat.completions.create({
|
|
198
|
+
model: request.model || 'gpt-4-turbo',
|
|
199
|
+
messages: request.messages.map(msg => ({
|
|
200
|
+
role: msg.role,
|
|
201
|
+
content: msg.content
|
|
202
|
+
})),
|
|
203
|
+
functions: request.functions,
|
|
204
|
+
function_call: 'auto'
|
|
205
|
+
});
|
|
206
|
+
const choice = completion.choices[0];
|
|
207
|
+
const finishReason = choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call'
|
|
208
|
+
? 'function_call'
|
|
209
|
+
: choice.finish_reason;
|
|
210
|
+
return {
|
|
211
|
+
success: true,
|
|
212
|
+
content: choice.message.content || '',
|
|
213
|
+
finishReason,
|
|
214
|
+
usage: completion.usage ? {
|
|
215
|
+
promptTokens: completion.usage.prompt_tokens,
|
|
216
|
+
completionTokens: completion.usage.completion_tokens,
|
|
217
|
+
totalTokens: completion.usage.total_tokens
|
|
218
|
+
} : undefined,
|
|
219
|
+
model: completion.model,
|
|
220
|
+
metadata: {
|
|
221
|
+
functionCall: choice.message.function_call
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
const aiError = (0, error_handling_1.handleOpenAIError)(error);
|
|
227
|
+
return {
|
|
228
|
+
success: false,
|
|
229
|
+
error: aiError.message
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async analyzeImage(imageUrl, prompt, model = 'gpt-4-vision-preview') {
|
|
234
|
+
try {
|
|
235
|
+
const completion = await this.client.chat.completions.create({
|
|
236
|
+
model,
|
|
237
|
+
messages: [
|
|
238
|
+
{
|
|
239
|
+
role: 'user',
|
|
240
|
+
content: [
|
|
241
|
+
{ type: 'text', text: prompt },
|
|
242
|
+
{ type: 'image_url', image_url: { url: imageUrl } }
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
],
|
|
246
|
+
max_tokens: 1000
|
|
247
|
+
});
|
|
248
|
+
const choice = completion.choices[0];
|
|
249
|
+
const finishReason = choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call'
|
|
250
|
+
? 'function_call'
|
|
251
|
+
: choice.finish_reason;
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
content: choice.message.content || '',
|
|
255
|
+
finishReason,
|
|
256
|
+
usage: completion.usage ? {
|
|
257
|
+
promptTokens: completion.usage.prompt_tokens,
|
|
258
|
+
completionTokens: completion.usage.completion_tokens,
|
|
259
|
+
totalTokens: completion.usage.total_tokens
|
|
260
|
+
} : undefined,
|
|
261
|
+
model: completion.model
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
const aiError = (0, error_handling_1.handleOpenAIError)(error);
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
error: aiError.message
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
estimateCost(request) {
|
|
273
|
+
const model = request.model || this.config.defaultModel || 'gpt-4-turbo';
|
|
274
|
+
const pricing = this.getModelPricing(model);
|
|
275
|
+
const inputTokens = this.estimateTokens(request.messages.map(m => m.content).join(' '));
|
|
276
|
+
const outputTokens = request.maxTokens || 1000;
|
|
277
|
+
const inputCost = (inputTokens / 1000) * pricing.inputPrice;
|
|
278
|
+
const outputCost = (outputTokens / 1000) * pricing.outputPrice;
|
|
279
|
+
return {
|
|
280
|
+
inputTokens,
|
|
281
|
+
outputTokens,
|
|
282
|
+
totalTokens: inputTokens + outputTokens,
|
|
283
|
+
estimatedCostUSD: inputCost + outputCost,
|
|
284
|
+
currency: 'USD'
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
initializeModels() {
|
|
288
|
+
model_registry_1.OPENAI_MODELS.forEach(model => {
|
|
289
|
+
this.models.set(model.id, model);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
mapOpenAIModel(model) {
|
|
293
|
+
const cached = this.models.get(model.id);
|
|
294
|
+
if (cached) {
|
|
295
|
+
return cached;
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
id: model.id,
|
|
299
|
+
name: model.id,
|
|
300
|
+
contextWindow: 8192,
|
|
301
|
+
maxOutputTokens: 4096,
|
|
302
|
+
capabilities: ['chat']
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
getModelPricing(model) {
|
|
306
|
+
const modelInfo = this.models.get(model);
|
|
307
|
+
if (modelInfo?.pricing) {
|
|
308
|
+
return {
|
|
309
|
+
inputPrice: modelInfo.pricing.inputPricePerToken * 1000,
|
|
310
|
+
outputPrice: modelInfo.pricing.outputPricePerToken * 1000
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
inputPrice: 0.0005,
|
|
315
|
+
outputPrice: 0.0015
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
exports.OpenAIProvider = OpenAIProvider;
|
|
320
|
+
//# sourceMappingURL=OpenAIProvider.js.map
|