@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.
@@ -0,0 +1,435 @@
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
+ AIProvider,
11
+ CompletionRequest,
12
+ CompletionResponse,
13
+ StreamChunk,
14
+ EmbeddingRequest,
15
+ EmbeddingResponse,
16
+ ModerationResponse,
17
+ ModelInfo,
18
+ HealthStatus,
19
+ CostEstimate
20
+ } from '@bernierllc/ai-provider-core';
21
+ import OpenAI from 'openai';
22
+ import { OpenAIProviderConfig, OpenAIFunction } from './types/openai-types';
23
+ import { OPENAI_MODELS } from './models/model-registry';
24
+ import { handleOpenAIError } from './utils/error-handling';
25
+
26
+ /**
27
+ * OpenAI Provider Implementation
28
+ * Implements the unified AI provider interface for OpenAI's API
29
+ */
30
+ export class OpenAIProvider extends AIProvider {
31
+ private client: OpenAI;
32
+ private models: Map<string, ModelInfo> = new Map();
33
+
34
+ constructor(config: OpenAIProviderConfig) {
35
+ super(config);
36
+
37
+ this.client = new OpenAI({
38
+ apiKey: config.apiKey,
39
+ organization: config.organizationId,
40
+ baseURL: config.baseURL,
41
+ timeout: config.timeout || 60000,
42
+ maxRetries: config.maxRetries || 3
43
+ });
44
+
45
+ this.initializeModels();
46
+ }
47
+
48
+ // ============================================
49
+ // CORE OPERATIONS (REQUIRED)
50
+ // ============================================
51
+
52
+ /**
53
+ * Generate text completion using OpenAI API
54
+ */
55
+ async complete(request: CompletionRequest): Promise<CompletionResponse> {
56
+ // Validate request
57
+ const validation = this.validateRequest(request);
58
+ if (!validation.isValid) {
59
+ return {
60
+ success: false,
61
+ error: validation.errors.join(', ')
62
+ };
63
+ }
64
+
65
+ try {
66
+ const completion = await this.client.chat.completions.create({
67
+ model: request.model || this.config.defaultModel || 'gpt-4-turbo',
68
+ messages: request.messages.map(msg => ({
69
+ role: msg.role as 'system' | 'user' | 'assistant',
70
+ content: msg.content,
71
+ name: msg.name
72
+ })),
73
+ max_tokens: request.maxTokens,
74
+ temperature: request.temperature,
75
+ top_p: request.topP,
76
+ frequency_penalty: request.frequencyPenalty,
77
+ presence_penalty: request.presencePenalty,
78
+ stop: request.stop,
79
+ user: request.user
80
+ });
81
+
82
+ const choice = completion.choices[0];
83
+
84
+ // Map OpenAI's finish_reason to base interface types
85
+ const finishReason = choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call'
86
+ ? 'function_call' as const
87
+ : choice.finish_reason as 'stop' | 'length' | 'content_filter' | undefined;
88
+
89
+ return {
90
+ success: true,
91
+ content: choice.message.content || '',
92
+ finishReason,
93
+ usage: completion.usage ? {
94
+ promptTokens: completion.usage.prompt_tokens,
95
+ completionTokens: completion.usage.completion_tokens,
96
+ totalTokens: completion.usage.total_tokens
97
+ } : undefined,
98
+ model: completion.model,
99
+ metadata: {
100
+ id: completion.id,
101
+ created: completion.created,
102
+ systemFingerprint: completion.system_fingerprint
103
+ }
104
+ };
105
+ } catch (error) {
106
+ const aiError = handleOpenAIError(error);
107
+ return {
108
+ success: false,
109
+ error: aiError.message
110
+ };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Generate streaming text completion
116
+ */
117
+ async *streamComplete(
118
+ request: CompletionRequest
119
+ ): AsyncGenerator<StreamChunk, void, unknown> {
120
+ const validation = this.validateRequest(request);
121
+ if (!validation.isValid) {
122
+ throw new Error(validation.errors.join(', '));
123
+ }
124
+
125
+ try {
126
+ const stream = await this.client.chat.completions.create({
127
+ model: request.model || this.config.defaultModel || 'gpt-4-turbo',
128
+ messages: request.messages.map(msg => ({
129
+ role: msg.role as 'system' | 'user' | 'assistant',
130
+ content: msg.content
131
+ })),
132
+ max_tokens: request.maxTokens,
133
+ temperature: request.temperature,
134
+ stream: true
135
+ });
136
+
137
+ for await (const chunk of stream) {
138
+ const delta = chunk.choices[0]?.delta?.content || '';
139
+ const rawFinishReason = chunk.choices[0]?.finish_reason;
140
+
141
+ // Map finish_reason for streaming (StreamChunk doesn't include function_call)
142
+ const finishReason = (rawFinishReason === 'stop' || rawFinishReason === 'length' || rawFinishReason === 'content_filter')
143
+ ? rawFinishReason
144
+ : undefined;
145
+
146
+ yield {
147
+ delta,
148
+ finishReason,
149
+ usage: chunk.usage ? {
150
+ promptTokens: chunk.usage.prompt_tokens,
151
+ completionTokens: chunk.usage.completion_tokens,
152
+ totalTokens: chunk.usage.total_tokens
153
+ } : undefined
154
+ };
155
+ }
156
+ } catch (error) {
157
+ throw handleOpenAIError(error);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Generate embeddings using OpenAI embeddings API
163
+ */
164
+ async generateEmbeddings(
165
+ request: EmbeddingRequest
166
+ ): Promise<EmbeddingResponse> {
167
+ try {
168
+ const response = await this.client.embeddings.create({
169
+ model: request.model || 'text-embedding-3-small',
170
+ input: request.input,
171
+ user: request.user
172
+ });
173
+
174
+ return {
175
+ success: true,
176
+ embeddings: response.data.map(item => item.embedding),
177
+ usage: {
178
+ promptTokens: response.usage.prompt_tokens,
179
+ completionTokens: 0,
180
+ totalTokens: response.usage.total_tokens
181
+ },
182
+ model: response.model
183
+ };
184
+ } catch (error) {
185
+ const aiError = handleOpenAIError(error);
186
+ return {
187
+ success: false,
188
+ error: aiError.message
189
+ };
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Check content moderation using OpenAI moderation API
195
+ */
196
+ async moderate(content: string): Promise<ModerationResponse> {
197
+ try {
198
+ const response = await this.client.moderations.create({
199
+ input: content
200
+ });
201
+
202
+ const result = response.results[0];
203
+
204
+ return {
205
+ success: true,
206
+ flagged: result.flagged,
207
+ categories: result.categories as unknown as Record<string, boolean>,
208
+ categoryScores: result.category_scores as unknown as Record<string, number>
209
+ };
210
+ } catch (error) {
211
+ const aiError = handleOpenAIError(error);
212
+ return {
213
+ success: false,
214
+ flagged: false,
215
+ error: aiError.message
216
+ };
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Get available OpenAI models
222
+ */
223
+ async getAvailableModels(): Promise<ModelInfo[]> {
224
+ try {
225
+ const response = await this.client.models.list();
226
+
227
+ return response.data
228
+ .filter(model =>
229
+ model.id.startsWith('gpt-') ||
230
+ model.id.startsWith('text-embedding')
231
+ )
232
+ .map(model => this.mapOpenAIModel(model));
233
+ } catch {
234
+ // Return cached models if API fails
235
+ return Array.from(this.models.values());
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Check OpenAI API health
241
+ */
242
+ async checkHealth(): Promise<HealthStatus> {
243
+ const startTime = Date.now();
244
+
245
+ try {
246
+ // Simple test request to verify API connectivity
247
+ await this.client.models.retrieve('gpt-3.5-turbo');
248
+
249
+ return {
250
+ status: 'healthy',
251
+ latency: Date.now() - startTime,
252
+ lastChecked: new Date()
253
+ };
254
+ } catch (error) {
255
+ return {
256
+ status: 'unavailable',
257
+ latency: Date.now() - startTime,
258
+ lastChecked: new Date(),
259
+ details: {
260
+ error: error instanceof Error ? error.message : 'Unknown error'
261
+ }
262
+ };
263
+ }
264
+ }
265
+
266
+ // ============================================
267
+ // OPENAI-SPECIFIC FEATURES
268
+ // ============================================
269
+
270
+ /**
271
+ * Chat completion with function calling
272
+ */
273
+ async completionWithFunctions(
274
+ request: CompletionRequest & { functions: OpenAIFunction[] }
275
+ ): Promise<CompletionResponse> {
276
+ try {
277
+ const completion = await this.client.chat.completions.create({
278
+ model: request.model || 'gpt-4-turbo',
279
+ messages: request.messages.map(msg => ({
280
+ role: msg.role as 'system' | 'user' | 'assistant',
281
+ content: msg.content
282
+ })),
283
+ functions: request.functions,
284
+ function_call: 'auto'
285
+ });
286
+
287
+ const choice = completion.choices[0];
288
+
289
+ // Map finish_reason to base types
290
+ const finishReason = choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call'
291
+ ? 'function_call' as const
292
+ : choice.finish_reason as 'stop' | 'length' | 'content_filter' | undefined;
293
+
294
+ return {
295
+ success: true,
296
+ content: choice.message.content || '',
297
+ finishReason,
298
+ usage: completion.usage ? {
299
+ promptTokens: completion.usage.prompt_tokens,
300
+ completionTokens: completion.usage.completion_tokens,
301
+ totalTokens: completion.usage.total_tokens
302
+ } : undefined,
303
+ model: completion.model,
304
+ metadata: {
305
+ functionCall: choice.message.function_call
306
+ }
307
+ };
308
+ } catch (error) {
309
+ const aiError = handleOpenAIError(error);
310
+ return {
311
+ success: false,
312
+ error: aiError.message
313
+ };
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Vision capabilities (GPT-4 Vision)
319
+ */
320
+ async analyzeImage(
321
+ imageUrl: string,
322
+ prompt: string,
323
+ model: string = 'gpt-4-vision-preview'
324
+ ): Promise<CompletionResponse> {
325
+ try {
326
+ const completion = await this.client.chat.completions.create({
327
+ model,
328
+ messages: [
329
+ {
330
+ role: 'user',
331
+ content: [
332
+ { type: 'text', text: prompt },
333
+ { type: 'image_url', image_url: { url: imageUrl } }
334
+ ]
335
+ }
336
+ ],
337
+ max_tokens: 1000
338
+ });
339
+
340
+ const choice = completion.choices[0];
341
+
342
+ // Map finish_reason to base types
343
+ const finishReason = choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call'
344
+ ? 'function_call' as const
345
+ : choice.finish_reason as 'stop' | 'length' | 'content_filter' | undefined;
346
+
347
+ return {
348
+ success: true,
349
+ content: choice.message.content || '',
350
+ finishReason,
351
+ usage: completion.usage ? {
352
+ promptTokens: completion.usage.prompt_tokens,
353
+ completionTokens: completion.usage.completion_tokens,
354
+ totalTokens: completion.usage.total_tokens
355
+ } : undefined,
356
+ model: completion.model
357
+ };
358
+ } catch (error) {
359
+ const aiError = handleOpenAIError(error);
360
+ return {
361
+ success: false,
362
+ error: aiError.message
363
+ };
364
+ }
365
+ }
366
+
367
+ // ============================================
368
+ // COST ESTIMATION (OVERRIDE)
369
+ // ============================================
370
+
371
+ /**
372
+ * Estimate cost using OpenAI pricing
373
+ */
374
+ estimateCost(request: CompletionRequest): CostEstimate {
375
+ const model = request.model || this.config.defaultModel || 'gpt-4-turbo';
376
+ const pricing = this.getModelPricing(model);
377
+
378
+ const inputTokens = this.estimateTokens(
379
+ request.messages.map(m => m.content).join(' ')
380
+ );
381
+ const outputTokens = request.maxTokens || 1000;
382
+
383
+ const inputCost = (inputTokens / 1000) * pricing.inputPrice;
384
+ const outputCost = (outputTokens / 1000) * pricing.outputPrice;
385
+
386
+ return {
387
+ inputTokens,
388
+ outputTokens,
389
+ totalTokens: inputTokens + outputTokens,
390
+ estimatedCostUSD: inputCost + outputCost,
391
+ currency: 'USD'
392
+ };
393
+ }
394
+
395
+ // ============================================
396
+ // PRIVATE METHODS
397
+ // ============================================
398
+
399
+ private initializeModels() {
400
+ OPENAI_MODELS.forEach(model => {
401
+ this.models.set(model.id, model);
402
+ });
403
+ }
404
+
405
+ private mapOpenAIModel(model: OpenAI.Model): ModelInfo {
406
+ const cached = this.models.get(model.id);
407
+ if (cached) {
408
+ return cached;
409
+ }
410
+
411
+ return {
412
+ id: model.id,
413
+ name: model.id,
414
+ contextWindow: 8192, // Default
415
+ maxOutputTokens: 4096, // Default
416
+ capabilities: ['chat']
417
+ };
418
+ }
419
+
420
+ private getModelPricing(model: string): { inputPrice: number; outputPrice: number } {
421
+ const modelInfo = this.models.get(model);
422
+ if (modelInfo?.pricing) {
423
+ return {
424
+ inputPrice: modelInfo.pricing.inputPricePerToken * 1000,
425
+ outputPrice: modelInfo.pricing.outputPricePerToken * 1000
426
+ };
427
+ }
428
+
429
+ // Default pricing (gpt-3.5-turbo)
430
+ return {
431
+ inputPrice: 0.0005,
432
+ outputPrice: 0.0015
433
+ };
434
+ }
435
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
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
+ export { OpenAIProvider } from './OpenAIProvider';
10
+ export * from './types/openai-types';
11
+ export * from './models/model-registry';
12
+ export * from './utils/error-handling';
@@ -0,0 +1,178 @@
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 { ModelInfo } from '@bernierllc/ai-provider-core';
10
+
11
+ /**
12
+ * OpenAI Model Registry
13
+ * Contains information about all available OpenAI models
14
+ */
15
+ export const OPENAI_MODELS: ModelInfo[] = [
16
+ {
17
+ id: 'gpt-4-turbo',
18
+ name: 'GPT-4 Turbo',
19
+ contextWindow: 128000,
20
+ maxOutputTokens: 4096,
21
+ pricing: {
22
+ inputPricePerToken: 0.01 / 1000,
23
+ outputPricePerToken: 0.03 / 1000,
24
+ currency: 'USD'
25
+ },
26
+ capabilities: ['chat', 'completion', 'function-calling', 'vision']
27
+ },
28
+ {
29
+ id: 'gpt-4-turbo-preview',
30
+ name: 'GPT-4 Turbo Preview',
31
+ contextWindow: 128000,
32
+ maxOutputTokens: 4096,
33
+ pricing: {
34
+ inputPricePerToken: 0.01 / 1000,
35
+ outputPricePerToken: 0.03 / 1000,
36
+ currency: 'USD'
37
+ },
38
+ capabilities: ['chat', 'completion', 'function-calling']
39
+ },
40
+ {
41
+ id: 'gpt-4',
42
+ name: 'GPT-4',
43
+ contextWindow: 8192,
44
+ maxOutputTokens: 8192,
45
+ pricing: {
46
+ inputPricePerToken: 0.03 / 1000,
47
+ outputPricePerToken: 0.06 / 1000,
48
+ currency: 'USD'
49
+ },
50
+ capabilities: ['chat', 'completion', 'function-calling']
51
+ },
52
+ {
53
+ id: 'gpt-4-32k',
54
+ name: 'GPT-4 32K',
55
+ contextWindow: 32768,
56
+ maxOutputTokens: 32768,
57
+ pricing: {
58
+ inputPricePerToken: 0.06 / 1000,
59
+ outputPricePerToken: 0.12 / 1000,
60
+ currency: 'USD'
61
+ },
62
+ capabilities: ['chat', 'completion', 'function-calling']
63
+ },
64
+ {
65
+ id: 'gpt-3.5-turbo',
66
+ name: 'GPT-3.5 Turbo',
67
+ contextWindow: 16385,
68
+ maxOutputTokens: 4096,
69
+ pricing: {
70
+ inputPricePerToken: 0.0005 / 1000,
71
+ outputPricePerToken: 0.0015 / 1000,
72
+ currency: 'USD'
73
+ },
74
+ capabilities: ['chat', 'completion', 'function-calling']
75
+ },
76
+ {
77
+ id: 'gpt-3.5-turbo-16k',
78
+ name: 'GPT-3.5 Turbo 16K',
79
+ contextWindow: 16385,
80
+ maxOutputTokens: 16385,
81
+ pricing: {
82
+ inputPricePerToken: 0.003 / 1000,
83
+ outputPricePerToken: 0.004 / 1000,
84
+ currency: 'USD'
85
+ },
86
+ capabilities: ['chat', 'completion', 'function-calling']
87
+ },
88
+ {
89
+ id: 'gpt-4-vision-preview',
90
+ name: 'GPT-4 Vision Preview',
91
+ contextWindow: 128000,
92
+ maxOutputTokens: 4096,
93
+ pricing: {
94
+ inputPricePerToken: 0.01 / 1000,
95
+ outputPricePerToken: 0.03 / 1000,
96
+ currency: 'USD'
97
+ },
98
+ capabilities: ['chat', 'completion', 'vision']
99
+ },
100
+ {
101
+ id: 'text-embedding-3-small',
102
+ name: 'Text Embedding 3 Small',
103
+ contextWindow: 8191,
104
+ maxOutputTokens: 0,
105
+ pricing: {
106
+ inputPricePerToken: 0.00002 / 1000,
107
+ outputPricePerToken: 0,
108
+ currency: 'USD'
109
+ },
110
+ capabilities: ['embeddings']
111
+ },
112
+ {
113
+ id: 'text-embedding-3-large',
114
+ name: 'Text Embedding 3 Large',
115
+ contextWindow: 8191,
116
+ maxOutputTokens: 0,
117
+ pricing: {
118
+ inputPricePerToken: 0.00013 / 1000,
119
+ outputPricePerToken: 0,
120
+ currency: 'USD'
121
+ },
122
+ capabilities: ['embeddings']
123
+ },
124
+ {
125
+ id: 'text-embedding-ada-002',
126
+ name: 'Text Embedding Ada 002',
127
+ contextWindow: 8191,
128
+ maxOutputTokens: 0,
129
+ pricing: {
130
+ inputPricePerToken: 0.0001 / 1000,
131
+ outputPricePerToken: 0,
132
+ currency: 'USD'
133
+ },
134
+ capabilities: ['embeddings']
135
+ }
136
+ ];
137
+
138
+ /**
139
+ * Get model information by ID
140
+ */
141
+ export function getModelInfo(modelId: string): ModelInfo | undefined {
142
+ return OPENAI_MODELS.find(model => model.id === modelId);
143
+ }
144
+
145
+ /**
146
+ * Get all chat models
147
+ */
148
+ export function getChatModels(): ModelInfo[] {
149
+ return OPENAI_MODELS.filter(model =>
150
+ model.capabilities.includes('chat') || model.capabilities.includes('completion')
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Get all embedding models
156
+ */
157
+ export function getEmbeddingModels(): ModelInfo[] {
158
+ return OPENAI_MODELS.filter(model =>
159
+ model.capabilities.includes('embeddings')
160
+ );
161
+ }
162
+
163
+ /**
164
+ * Get all vision-capable models
165
+ */
166
+ export function getVisionModels(): ModelInfo[] {
167
+ return OPENAI_MODELS.filter(model =>
168
+ model.capabilities.includes('vision')
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Check if model supports function calling
174
+ */
175
+ export function supportsFunctionCalling(modelId: string): boolean {
176
+ const model = getModelInfo(modelId);
177
+ return model?.capabilities.includes('function-calling') ?? false;
178
+ }
@@ -0,0 +1,51 @@
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 { AIProviderConfig } from '@bernierllc/ai-provider-core';
10
+
11
+ /**
12
+ * OpenAI Provider Configuration
13
+ */
14
+ export interface OpenAIProviderConfig extends AIProviderConfig {
15
+ providerName: 'openai';
16
+ organizationId?: string;
17
+ baseURL?: string;
18
+ timeout?: number;
19
+ maxRetries?: number;
20
+ }
21
+
22
+ /**
23
+ * OpenAI Function Definition (for function calling)
24
+ */
25
+ export interface OpenAIFunction {
26
+ name: string;
27
+ description: string;
28
+ parameters: {
29
+ type: 'object';
30
+ properties: Record<string, unknown>;
31
+ required?: string[];
32
+ };
33
+ }
34
+
35
+ /**
36
+ * OpenAI Vision Request
37
+ */
38
+ export interface OpenAIVisionRequest {
39
+ imageUrl: string;
40
+ prompt: string;
41
+ model?: string;
42
+ maxTokens?: number;
43
+ }
44
+
45
+ /**
46
+ * OpenAI Model Pricing Information
47
+ */
48
+ export interface OpenAIModelPricing {
49
+ inputPrice: number; // USD per 1K tokens
50
+ outputPrice: number; // USD per 1K tokens
51
+ }