@sparkleideas/providers 3.5.2-patch.1

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,490 @@
1
+ /**
2
+ * V3 OpenAI Provider
3
+ *
4
+ * Supports GPT-4o, GPT-4, o1, and other OpenAI models.
5
+ *
6
+ * @module @sparkleideas/providers/openai-provider
7
+ */
8
+
9
+ import { BaseProvider, BaseProviderOptions } from './base-provider.js';
10
+ import {
11
+ LLMProvider,
12
+ LLMModel,
13
+ LLMRequest,
14
+ LLMResponse,
15
+ LLMStreamEvent,
16
+ ModelInfo,
17
+ ProviderCapabilities,
18
+ HealthCheckResult,
19
+ AuthenticationError,
20
+ RateLimitError,
21
+ ModelNotFoundError,
22
+ LLMProviderError,
23
+ } from './types.js';
24
+
25
+ interface OpenAIRequest {
26
+ model: string;
27
+ messages: Array<{
28
+ role: 'system' | 'user' | 'assistant' | 'tool';
29
+ content: string;
30
+ name?: string;
31
+ tool_call_id?: string;
32
+ tool_calls?: Array<{
33
+ id: string;
34
+ type: 'function';
35
+ function: { name: string; arguments: string };
36
+ }>;
37
+ }>;
38
+ temperature?: number;
39
+ max_tokens?: number;
40
+ top_p?: number;
41
+ frequency_penalty?: number;
42
+ presence_penalty?: number;
43
+ stop?: string[];
44
+ stream?: boolean;
45
+ tools?: Array<{
46
+ type: 'function';
47
+ function: {
48
+ name: string;
49
+ description: string;
50
+ parameters: unknown;
51
+ };
52
+ }>;
53
+ tool_choice?: 'auto' | 'none' | 'required' | { type: 'function'; function: { name: string } };
54
+ }
55
+
56
+ interface OpenAIResponse {
57
+ id: string;
58
+ object: string;
59
+ created: number;
60
+ model: string;
61
+ choices: Array<{
62
+ index: number;
63
+ message: {
64
+ role: string;
65
+ content: string | null;
66
+ tool_calls?: Array<{
67
+ id: string;
68
+ type: 'function';
69
+ function: { name: string; arguments: string };
70
+ }>;
71
+ };
72
+ finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter';
73
+ }>;
74
+ usage: {
75
+ prompt_tokens: number;
76
+ completion_tokens: number;
77
+ total_tokens: number;
78
+ };
79
+ }
80
+
81
+ export class OpenAIProvider extends BaseProvider {
82
+ readonly name: LLMProvider = 'openai';
83
+ readonly capabilities: ProviderCapabilities = {
84
+ supportedModels: [
85
+ 'gpt-4o',
86
+ 'gpt-4o-mini',
87
+ 'gpt-4-turbo',
88
+ 'gpt-4',
89
+ 'gpt-3.5-turbo',
90
+ 'o1-preview',
91
+ 'o1-mini',
92
+ 'o3-mini',
93
+ ],
94
+ maxContextLength: {
95
+ 'gpt-4o': 128000,
96
+ 'gpt-4o-mini': 128000,
97
+ 'gpt-4-turbo': 128000,
98
+ 'gpt-4': 8192,
99
+ 'gpt-3.5-turbo': 16384,
100
+ 'o1-preview': 128000,
101
+ 'o1-mini': 128000,
102
+ 'o3-mini': 200000,
103
+ },
104
+ maxOutputTokens: {
105
+ 'gpt-4o': 16384,
106
+ 'gpt-4o-mini': 16384,
107
+ 'gpt-4-turbo': 4096,
108
+ 'gpt-4': 8192,
109
+ 'gpt-3.5-turbo': 4096,
110
+ 'o1-preview': 32768,
111
+ 'o1-mini': 65536,
112
+ 'o3-mini': 100000,
113
+ },
114
+ supportsStreaming: true,
115
+ supportsToolCalling: true,
116
+ supportsSystemMessages: true,
117
+ supportsVision: true,
118
+ supportsAudio: true,
119
+ supportsFineTuning: true,
120
+ supportsEmbeddings: true,
121
+ supportsBatching: true,
122
+ rateLimit: {
123
+ requestsPerMinute: 10000,
124
+ tokensPerMinute: 2000000,
125
+ concurrentRequests: 500,
126
+ },
127
+ pricing: {
128
+ 'gpt-4o': {
129
+ promptCostPer1k: 0.0025,
130
+ completionCostPer1k: 0.01,
131
+ currency: 'USD',
132
+ },
133
+ 'gpt-4o-mini': {
134
+ promptCostPer1k: 0.00015,
135
+ completionCostPer1k: 0.0006,
136
+ currency: 'USD',
137
+ },
138
+ 'gpt-4-turbo': {
139
+ promptCostPer1k: 0.01,
140
+ completionCostPer1k: 0.03,
141
+ currency: 'USD',
142
+ },
143
+ 'gpt-4': {
144
+ promptCostPer1k: 0.03,
145
+ completionCostPer1k: 0.06,
146
+ currency: 'USD',
147
+ },
148
+ 'gpt-3.5-turbo': {
149
+ promptCostPer1k: 0.0005,
150
+ completionCostPer1k: 0.0015,
151
+ currency: 'USD',
152
+ },
153
+ 'o1-preview': {
154
+ promptCostPer1k: 0.015,
155
+ completionCostPer1k: 0.06,
156
+ currency: 'USD',
157
+ },
158
+ 'o1-mini': {
159
+ promptCostPer1k: 0.003,
160
+ completionCostPer1k: 0.012,
161
+ currency: 'USD',
162
+ },
163
+ 'o3-mini': {
164
+ promptCostPer1k: 0.0011,
165
+ completionCostPer1k: 0.0044,
166
+ currency: 'USD',
167
+ },
168
+ },
169
+ };
170
+
171
+ private baseUrl: string = 'https://api.openai.com/v1';
172
+ private headers: Record<string, string> = {};
173
+
174
+ constructor(options: BaseProviderOptions) {
175
+ super(options);
176
+ }
177
+
178
+ protected async doInitialize(): Promise<void> {
179
+ if (!this.config.apiKey) {
180
+ throw new AuthenticationError('OpenAI API key is required', 'openai');
181
+ }
182
+
183
+ this.baseUrl = this.config.apiUrl || 'https://api.openai.com/v1';
184
+ this.headers = {
185
+ Authorization: `Bearer ${this.config.apiKey}`,
186
+ 'Content-Type': 'application/json',
187
+ };
188
+
189
+ if (this.config.providerOptions?.organization) {
190
+ this.headers['OpenAI-Organization'] = this.config.providerOptions.organization as string;
191
+ }
192
+ }
193
+
194
+ protected async doComplete(request: LLMRequest): Promise<LLMResponse> {
195
+ const openAIRequest = this.buildRequest(request);
196
+
197
+ const controller = new AbortController();
198
+ const timeout = setTimeout(() => controller.abort(), this.config.timeout || 60000);
199
+
200
+ try {
201
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
202
+ method: 'POST',
203
+ headers: this.headers,
204
+ body: JSON.stringify(openAIRequest),
205
+ signal: controller.signal,
206
+ });
207
+
208
+ clearTimeout(timeout);
209
+
210
+ if (!response.ok) {
211
+ await this.handleErrorResponse(response);
212
+ }
213
+
214
+ const data = await response.json() as OpenAIResponse;
215
+ return this.transformResponse(data, request);
216
+ } catch (error) {
217
+ clearTimeout(timeout);
218
+ throw this.transformError(error);
219
+ }
220
+ }
221
+
222
+ protected async *doStreamComplete(request: LLMRequest): AsyncIterable<LLMStreamEvent> {
223
+ const openAIRequest = this.buildRequest(request, true);
224
+
225
+ const controller = new AbortController();
226
+ const timeout = setTimeout(() => controller.abort(), (this.config.timeout || 60000) * 2);
227
+
228
+ try {
229
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
230
+ method: 'POST',
231
+ headers: this.headers,
232
+ body: JSON.stringify(openAIRequest),
233
+ signal: controller.signal,
234
+ });
235
+
236
+ if (!response.ok) {
237
+ await this.handleErrorResponse(response);
238
+ }
239
+
240
+ const reader = response.body!.getReader();
241
+ const decoder = new TextDecoder();
242
+ let buffer = '';
243
+
244
+ while (true) {
245
+ const { done, value } = await reader.read();
246
+ if (done) break;
247
+
248
+ buffer += decoder.decode(value, { stream: true });
249
+ const lines = buffer.split('\n');
250
+ buffer = lines.pop() || '';
251
+
252
+ for (const line of lines) {
253
+ if (line.startsWith('data: ')) {
254
+ const data = line.slice(6);
255
+ if (data === '[DONE]') {
256
+ // Estimate final usage
257
+ const promptTokens = this.estimateTokens(JSON.stringify(request.messages));
258
+ const model = request.model || this.config.model;
259
+ const baseModel = model.includes('/') ? model.split('/').pop()! : model;
260
+ const pricing = this.capabilities.pricing[model] || this.capabilities.pricing[baseModel];
261
+ const promptCostPer1k = pricing?.promptCostPer1k ?? 0;
262
+ const completionCostPer1k = pricing?.completionCostPer1k ?? 0;
263
+
264
+ yield {
265
+ type: 'done',
266
+ usage: {
267
+ promptTokens,
268
+ completionTokens: 100, // Estimate
269
+ totalTokens: promptTokens + 100,
270
+ },
271
+ cost: {
272
+ promptCost: (promptTokens / 1000) * promptCostPer1k,
273
+ completionCost: (100 / 1000) * completionCostPer1k,
274
+ totalCost:
275
+ (promptTokens / 1000) * promptCostPer1k +
276
+ (100 / 1000) * completionCostPer1k,
277
+ currency: 'USD',
278
+ },
279
+ };
280
+ continue;
281
+ }
282
+
283
+ try {
284
+ const chunk = JSON.parse(data);
285
+ const delta = chunk.choices?.[0]?.delta;
286
+
287
+ if (delta?.content) {
288
+ yield {
289
+ type: 'content',
290
+ delta: { content: delta.content },
291
+ };
292
+ }
293
+
294
+ if (delta?.tool_calls) {
295
+ for (const toolCall of delta.tool_calls) {
296
+ yield {
297
+ type: 'tool_call',
298
+ delta: {
299
+ toolCall: {
300
+ id: toolCall.id,
301
+ type: 'function',
302
+ function: toolCall.function,
303
+ },
304
+ },
305
+ };
306
+ }
307
+ }
308
+ } catch {
309
+ // Ignore parse errors
310
+ }
311
+ }
312
+ }
313
+ }
314
+ } catch (error) {
315
+ clearTimeout(timeout);
316
+ throw this.transformError(error);
317
+ } finally {
318
+ clearTimeout(timeout);
319
+ }
320
+ }
321
+
322
+ async listModels(): Promise<LLMModel[]> {
323
+ return this.capabilities.supportedModels;
324
+ }
325
+
326
+ async getModelInfo(model: LLMModel): Promise<ModelInfo> {
327
+ const descriptions: Record<string, string> = {
328
+ 'gpt-4o': 'Most capable GPT-4 model with vision and audio',
329
+ 'gpt-4o-mini': 'Affordable and intelligent small model',
330
+ 'gpt-4-turbo': 'GPT-4 Turbo with vision',
331
+ 'gpt-4': 'High capability model',
332
+ 'gpt-3.5-turbo': 'Fast and efficient model',
333
+ 'o1-preview': 'Reasoning model for complex tasks',
334
+ 'o1-mini': 'Fast reasoning model',
335
+ 'o3-mini': 'Latest reasoning model',
336
+ };
337
+
338
+ return {
339
+ model,
340
+ name: model,
341
+ description: descriptions[model] || 'OpenAI language model',
342
+ contextLength: this.capabilities.maxContextLength[model] || 8192,
343
+ maxOutputTokens: this.capabilities.maxOutputTokens[model] || 4096,
344
+ supportedFeatures: [
345
+ 'chat',
346
+ 'completion',
347
+ 'tool_calling',
348
+ ...(model.includes('gpt-4') ? ['vision'] : []),
349
+ ],
350
+ pricing: this.capabilities.pricing[model],
351
+ };
352
+ }
353
+
354
+ protected async doHealthCheck(): Promise<HealthCheckResult> {
355
+ try {
356
+ const response = await fetch(`${this.baseUrl}/models`, {
357
+ headers: this.headers,
358
+ });
359
+
360
+ return {
361
+ healthy: response.ok,
362
+ timestamp: new Date(),
363
+ ...(response.ok ? {} : { error: `HTTP ${response.status}` }),
364
+ };
365
+ } catch (error) {
366
+ return {
367
+ healthy: false,
368
+ error: error instanceof Error ? error.message : 'Unknown error',
369
+ timestamp: new Date(),
370
+ };
371
+ }
372
+ }
373
+
374
+ private buildRequest(request: LLMRequest, stream = false): OpenAIRequest {
375
+ const openAIRequest: OpenAIRequest = {
376
+ model: request.model || this.config.model,
377
+ messages: request.messages.map((msg) => ({
378
+ role: msg.role,
379
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
380
+ ...(msg.name && { name: msg.name }),
381
+ ...(msg.toolCallId && { tool_call_id: msg.toolCallId }),
382
+ ...(msg.toolCalls && { tool_calls: msg.toolCalls }),
383
+ })),
384
+ stream,
385
+ };
386
+
387
+ if (request.temperature !== undefined || this.config.temperature !== undefined) {
388
+ openAIRequest.temperature = request.temperature ?? this.config.temperature;
389
+ }
390
+
391
+ if (request.maxTokens || this.config.maxTokens) {
392
+ openAIRequest.max_tokens = request.maxTokens || this.config.maxTokens;
393
+ }
394
+
395
+ if (request.topP !== undefined || this.config.topP !== undefined) {
396
+ openAIRequest.top_p = request.topP ?? this.config.topP;
397
+ }
398
+
399
+ if (request.frequencyPenalty !== undefined || this.config.frequencyPenalty !== undefined) {
400
+ openAIRequest.frequency_penalty = request.frequencyPenalty ?? this.config.frequencyPenalty;
401
+ }
402
+
403
+ if (request.presencePenalty !== undefined || this.config.presencePenalty !== undefined) {
404
+ openAIRequest.presence_penalty = request.presencePenalty ?? this.config.presencePenalty;
405
+ }
406
+
407
+ if (request.stopSequences || this.config.stopSequences) {
408
+ openAIRequest.stop = request.stopSequences || this.config.stopSequences;
409
+ }
410
+
411
+ if (request.tools) {
412
+ openAIRequest.tools = request.tools;
413
+ openAIRequest.tool_choice = request.toolChoice;
414
+ }
415
+
416
+ return openAIRequest;
417
+ }
418
+
419
+ private transformResponse(data: OpenAIResponse, request: LLMRequest): LLMResponse {
420
+ const choice = data.choices[0];
421
+ const model = request.model || this.config.model;
422
+ // Handle OpenRouter and other compatible APIs with different model naming
423
+ const baseModel = model.includes('/') ? model.split('/').pop()! : model;
424
+ const pricing = this.capabilities.pricing[model] || this.capabilities.pricing[baseModel];
425
+
426
+ // Default pricing if model not found
427
+ const promptCostPer1k = pricing?.promptCostPer1k ?? 0;
428
+ const completionCostPer1k = pricing?.completionCostPer1k ?? 0;
429
+
430
+ const promptCost = (data.usage.prompt_tokens / 1000) * promptCostPer1k;
431
+ const completionCost = (data.usage.completion_tokens / 1000) * completionCostPer1k;
432
+
433
+ return {
434
+ id: data.id,
435
+ model: model as LLMModel,
436
+ provider: 'openai',
437
+ content: choice.message.content || '',
438
+ toolCalls: choice.message.tool_calls,
439
+ usage: {
440
+ promptTokens: data.usage.prompt_tokens,
441
+ completionTokens: data.usage.completion_tokens,
442
+ totalTokens: data.usage.total_tokens,
443
+ },
444
+ cost: {
445
+ promptCost,
446
+ completionCost,
447
+ totalCost: promptCost + completionCost,
448
+ currency: 'USD',
449
+ },
450
+ finishReason: choice.finish_reason,
451
+ };
452
+ }
453
+
454
+ private async handleErrorResponse(response: Response): Promise<never> {
455
+ const errorText = await response.text();
456
+ let errorData: { error?: { message?: string } };
457
+
458
+ try {
459
+ errorData = JSON.parse(errorText);
460
+ } catch {
461
+ errorData = { error: { message: errorText } };
462
+ }
463
+
464
+ const message = errorData.error?.message || 'Unknown error';
465
+
466
+ switch (response.status) {
467
+ case 401:
468
+ throw new AuthenticationError(message, 'openai', errorData);
469
+ case 429:
470
+ const retryAfter = response.headers.get('retry-after');
471
+ throw new RateLimitError(
472
+ message,
473
+ 'openai',
474
+ retryAfter ? parseInt(retryAfter) : undefined,
475
+ errorData
476
+ );
477
+ case 404:
478
+ throw new ModelNotFoundError(this.config.model, 'openai', errorData);
479
+ default:
480
+ throw new LLMProviderError(
481
+ message,
482
+ `OPENAI_${response.status}`,
483
+ 'openai',
484
+ response.status,
485
+ response.status >= 500,
486
+ errorData
487
+ );
488
+ }
489
+ }
490
+ }