llmjs2 0.0.2 → 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,485 @@
1
+ /**
2
+ * OpenAI provider implementation
3
+ */
4
+
5
+ import https from 'https';
6
+ import http from 'http';
7
+ import { URL } from 'url';
8
+
9
+ import {
10
+ CompletionRequest,
11
+ CompletionResponse,
12
+ CompletionChunk,
13
+ ProviderConfig,
14
+ } from '../types.js';
15
+ import {
16
+ BaseProvider,
17
+ validateCompletionRequest,
18
+ withRetry,
19
+ LLMError,
20
+ } from './base.js';
21
+
22
+ /**
23
+ * OpenAI API request format
24
+ */
25
+ interface OpenAIRequest {
26
+ model: string;
27
+ messages: Array<{
28
+ role: string;
29
+ content: string;
30
+ }>;
31
+ max_tokens?: number;
32
+ temperature?: number;
33
+ top_p?: number;
34
+ frequency_penalty?: number;
35
+ presence_penalty?: number;
36
+ stop?: string[];
37
+ tools?: Array<{
38
+ type: 'function';
39
+ function: {
40
+ name: string;
41
+ description?: string;
42
+ parameters?: Record<string, unknown>;
43
+ };
44
+ }>;
45
+ tool_choice?: 'auto' | 'required' | string;
46
+ stream?: boolean;
47
+ }
48
+
49
+ /**
50
+ * OpenAI API response format
51
+ */
52
+ interface OpenAIChoice {
53
+ message?: {
54
+ content: string | null;
55
+ tool_calls?: Array<{
56
+ id: string;
57
+ type: string;
58
+ function: {
59
+ name: string;
60
+ arguments: string;
61
+ };
62
+ }>;
63
+ };
64
+ delta?: {
65
+ content?: string;
66
+ tool_calls?: Array<{
67
+ index: number;
68
+ id?: string;
69
+ type?: string;
70
+ function?: {
71
+ name?: string;
72
+ arguments?: string;
73
+ };
74
+ }>;
75
+ };
76
+ finish_reason?: string;
77
+ }
78
+
79
+ interface OpenAIStreamResponse {
80
+ id: string;
81
+ object: string;
82
+ created: number;
83
+ model: string;
84
+ choices: OpenAIChoice[];
85
+ usage?: {
86
+ prompt_tokens: number;
87
+ completion_tokens: number;
88
+ total_tokens: number;
89
+ };
90
+ }
91
+
92
+ /**
93
+ * OpenAI Provider implementation
94
+ */
95
+ export class OpenAIProvider extends BaseProvider {
96
+ private apiKey: string;
97
+ private baseUrl: string = 'https://api.openai.com/v1';
98
+
99
+ constructor(config: ProviderConfig) {
100
+ super(config);
101
+
102
+ if (!config.apiKey) {
103
+ throw new Error('OpenAI API key is required');
104
+ }
105
+
106
+ this.apiKey = config.apiKey;
107
+
108
+ if (config.baseUrl) {
109
+ this.baseUrl = config.baseUrl;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Parse model string (e.g., 'openai/gpt-4' -> 'gpt-4')
115
+ */
116
+ parseModel(model: string): string {
117
+ if (model.startsWith('openai/')) {
118
+ return model.slice(7); // Remove 'openai/' prefix
119
+ }
120
+ return model;
121
+ }
122
+
123
+ /**
124
+ * Validate configuration
125
+ */
126
+ async validate(): Promise<void> {
127
+ try {
128
+ // Make a simple request to verify API key
129
+ await this.makeRequest('/models', 'GET');
130
+ this.logger('info', 'OpenAI API validation successful');
131
+ } catch (error) {
132
+ throw new LLMError(
133
+ `OpenAI validation failed: ${error instanceof Error ? error.message : String(error)}`,
134
+ 'VALIDATION_FAILED'
135
+ );
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Create a completion
141
+ */
142
+ async complete(request: CompletionRequest): Promise<CompletionResponse> {
143
+ validateCompletionRequest(request);
144
+
145
+ const model = this.parseModel(request.model);
146
+
147
+ return withRetry(
148
+ async () => {
149
+ const openaiRequest: OpenAIRequest = {
150
+ model,
151
+ messages: request.messages.map((msg) => ({
152
+ role: msg.role,
153
+ content: msg.content,
154
+ })),
155
+ };
156
+
157
+ // Add optional parameters
158
+ if (request.maxTokens) openaiRequest.max_tokens = request.maxTokens;
159
+ if (request.temperature !== undefined)
160
+ openaiRequest.temperature = request.temperature;
161
+ if (request.topP !== undefined) openaiRequest.top_p = request.topP;
162
+ if (request.frequencyPenalty !== undefined)
163
+ openaiRequest.frequency_penalty = request.frequencyPenalty;
164
+ if (request.presencePenalty !== undefined)
165
+ openaiRequest.presence_penalty = request.presencePenalty;
166
+ if (request.stop) openaiRequest.stop = request.stop;
167
+ if (request.tools) openaiRequest.tools = request.tools;
168
+ if (request.toolChoice) openaiRequest.tool_choice = request.toolChoice;
169
+
170
+ this.logger('debug', 'OpenAI completion request', {
171
+ model,
172
+ messageCount: request.messages.length,
173
+ });
174
+
175
+ const response = await this.makeRequest<OpenAIStreamResponse>(
176
+ '/chat/completions',
177
+ 'POST',
178
+ openaiRequest,
179
+ request
180
+ );
181
+
182
+ if (!response.choices || response.choices.length === 0) {
183
+ throw new LLMError('No choices in OpenAI response', 'NO_CHOICES');
184
+ }
185
+
186
+ const choice = response.choices[0];
187
+ const message = choice.message;
188
+
189
+ if (!message) {
190
+ throw new LLMError('No message in OpenAI response choice', 'NO_MESSAGE');
191
+ }
192
+
193
+ const result: CompletionResponse = {
194
+ content: message.content || '',
195
+ model: response.model,
196
+ stopReason: choice.finish_reason,
197
+ raw: response,
198
+ };
199
+
200
+ if (response.usage) {
201
+ result.usage = {
202
+ promptTokens: response.usage.prompt_tokens,
203
+ completionTokens: response.usage.completion_tokens,
204
+ totalTokens: response.usage.total_tokens,
205
+ };
206
+ }
207
+
208
+ if (message.tool_calls && message.tool_calls.length > 0) {
209
+ result.toolCalls = message.tool_calls.map((call) => ({
210
+ id: call.id,
211
+ name: call.function.name,
212
+ arguments: JSON.parse(call.function.arguments),
213
+ }));
214
+ }
215
+
216
+ this.logger('debug', 'OpenAI completion response', {
217
+ model: response.model,
218
+ tokens: response.usage?.total_tokens,
219
+ });
220
+
221
+ return result;
222
+ },
223
+ this.getRetryConfig(request)
224
+ );
225
+ }
226
+
227
+ /**
228
+ * Stream completion
229
+ */
230
+ async *completeStream(
231
+ request: CompletionRequest
232
+ ): AsyncIterable<CompletionChunk> {
233
+ validateCompletionRequest(request);
234
+
235
+ const model = this.parseModel(request.model);
236
+
237
+ const openaiRequest: OpenAIRequest = {
238
+ model,
239
+ stream: true,
240
+ messages: request.messages.map((msg) => ({
241
+ role: msg.role,
242
+ content: msg.content,
243
+ })),
244
+ };
245
+
246
+ if (request.maxTokens) openaiRequest.max_tokens = request.maxTokens;
247
+ if (request.temperature !== undefined)
248
+ openaiRequest.temperature = request.temperature;
249
+ if (request.topP !== undefined) openaiRequest.top_p = request.topP;
250
+ if (request.frequencyPenalty !== undefined)
251
+ openaiRequest.frequency_penalty = request.frequencyPenalty;
252
+ if (request.presencePenalty !== undefined)
253
+ openaiRequest.presence_penalty = request.presencePenalty;
254
+ if (request.stop) openaiRequest.stop = request.stop;
255
+ if (request.tools) openaiRequest.tools = request.tools;
256
+ if (request.toolChoice) openaiRequest.tool_choice = request.toolChoice;
257
+
258
+ this.logger('debug', 'OpenAI stream request', { model });
259
+
260
+ const stream = await this.makeStreamRequest(
261
+ '/chat/completions',
262
+ 'POST',
263
+ openaiRequest,
264
+ request
265
+ );
266
+
267
+ for await (const chunk of stream) {
268
+ if (chunk.choices && chunk.choices.length > 0) {
269
+ const choice = chunk.choices[0];
270
+
271
+ if (choice.delta?.content) {
272
+ yield {
273
+ delta: choice.delta.content,
274
+ stopReason: choice.finish_reason,
275
+ };
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Make HTTP request to OpenAI API
283
+ */
284
+ private makeRequest<T>(
285
+ path: string,
286
+ method: string = 'POST',
287
+ body?: unknown,
288
+ request?: CompletionRequest
289
+ ): Promise<T> {
290
+ return new Promise((resolve, reject) => {
291
+ const url = new URL(path, this.baseUrl);
292
+ const timeout = this.getTimeout(request);
293
+
294
+ const requestOptions = {
295
+ method,
296
+ headers: {
297
+ 'Content-Type': 'application/json',
298
+ Authorization: `Bearer ${this.apiKey}`,
299
+ ...this.getHeaders(request),
300
+ },
301
+ timeout,
302
+ };
303
+
304
+ const protocol = this.baseUrl.startsWith('https') ? https : http;
305
+
306
+ const req = protocol.request(url, requestOptions, (res) => {
307
+ let data = '';
308
+
309
+ res.on('data', (chunk) => {
310
+ data += chunk;
311
+ });
312
+
313
+ res.on('end', () => {
314
+ if (!res.statusCode || res.statusCode >= 400) {
315
+ try {
316
+ const errorData = JSON.parse(data);
317
+ reject(
318
+ new LLMError(
319
+ errorData.error?.message || 'OpenAI API error',
320
+ errorData.error?.code,
321
+ res.statusCode,
322
+ errorData,
323
+ res.statusCode === 429 ||
324
+ res.statusCode === 502 ||
325
+ res.statusCode === 503
326
+ )
327
+ );
328
+ } catch {
329
+ reject(
330
+ new LLMError(`OpenAI API error: ${data}`, 'API_ERROR', res.statusCode, null, false)
331
+ );
332
+ }
333
+ } else {
334
+ try {
335
+ resolve(JSON.parse(data) as T);
336
+ } catch (error) {
337
+ reject(
338
+ new LLMError(
339
+ 'Failed to parse OpenAI response',
340
+ 'PARSE_ERROR',
341
+ undefined,
342
+ { data }
343
+ )
344
+ );
345
+ }
346
+ }
347
+ });
348
+ });
349
+
350
+ req.on('error', (error) => {
351
+ reject(
352
+ new LLMError(
353
+ `OpenAI request failed: ${error.message}`,
354
+ 'REQUEST_FAILED',
355
+ undefined,
356
+ { error: error.message },
357
+ true
358
+ )
359
+ );
360
+ });
361
+
362
+ if (body) {
363
+ req.write(JSON.stringify(body));
364
+ }
365
+
366
+ req.end();
367
+ });
368
+ }
369
+
370
+ /**
371
+ * Stream HTTP request
372
+ */
373
+ private makeStreamRequest(
374
+ path: string,
375
+ method: string = 'POST',
376
+ body?: unknown,
377
+ request?: CompletionRequest
378
+ ): AsyncIterable<OpenAIStreamResponse> {
379
+ const self = this;
380
+
381
+ return {
382
+ async *[Symbol.asyncIterator]() {
383
+ const url = new URL(path, self.baseUrl);
384
+ const timeout = self.getTimeout(request);
385
+
386
+ const requestOptions = {
387
+ method,
388
+ headers: {
389
+ 'Content-Type': 'application/json',
390
+ Authorization: `Bearer ${self.apiKey}`,
391
+ ...self.getHeaders(request),
392
+ },
393
+ timeout,
394
+ };
395
+
396
+ const protocol = self.baseUrl.startsWith('https') ? https : http;
397
+ const chunks: OpenAIStreamResponse[] = [];
398
+
399
+ await new Promise<void>((resolve, reject) => {
400
+ const req = protocol.request(url, requestOptions, (res) => {
401
+ if (!res.statusCode || res.statusCode >= 400) {
402
+ let errorData = '';
403
+ res.on('data', (chunk) => {
404
+ errorData += chunk;
405
+ });
406
+ res.on('end', () => {
407
+ reject(
408
+ new LLMError(
409
+ `OpenAI stream error: ${errorData}`,
410
+ 'STREAM_ERROR',
411
+ res.statusCode
412
+ )
413
+ );
414
+ });
415
+ return;
416
+ }
417
+
418
+ let buffer = '';
419
+
420
+ res.on('data', (chunk) => {
421
+ buffer += chunk.toString();
422
+ const lines = buffer.split('\n');
423
+ buffer = lines[lines.length - 1];
424
+
425
+ for (let i = 0; i < lines.length - 1; i++) {
426
+ const line = lines[i].trim();
427
+
428
+ if (!line || line === '[DONE]') continue;
429
+
430
+ if (line.startsWith('data: ')) {
431
+ try {
432
+ const data = JSON.parse(line.slice(6)) as OpenAIStreamResponse;
433
+ chunks.push(data);
434
+ } catch (error) {
435
+ // Ignore parse errors in stream
436
+ }
437
+ }
438
+ }
439
+ });
440
+
441
+ res.on('end', () => {
442
+ resolve();
443
+ });
444
+ });
445
+
446
+ req.on('error', (error) => {
447
+ reject(
448
+ new LLMError(
449
+ `OpenAI stream request failed: ${error.message}`,
450
+ 'REQUEST_FAILED',
451
+ undefined,
452
+ { error: error.message },
453
+ true
454
+ )
455
+ );
456
+ });
457
+
458
+ req.on('timeout', () => {
459
+ req.destroy();
460
+ reject(
461
+ new LLMError(
462
+ 'OpenAI stream request timeout',
463
+ 'TIMEOUT',
464
+ undefined,
465
+ null,
466
+ true
467
+ )
468
+ );
469
+ });
470
+
471
+ if (body) {
472
+ req.write(JSON.stringify(body));
473
+ }
474
+
475
+ req.end();
476
+ });
477
+
478
+ // Yield collected chunks
479
+ for (const chunk of chunks) {
480
+ yield chunk;
481
+ }
482
+ },
483
+ };
484
+ }
485
+ }
package/src/types.ts ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Unified type definitions for llmjs2
3
+ * Enterprise-grade LLM abstraction layer supporting OpenAI and Ollama
4
+ */
5
+
6
+ /**
7
+ * Supported LLM providers
8
+ */
9
+ export type ProviderType = 'openai' | 'ollama';
10
+
11
+ /**
12
+ * Role of a message in a conversation
13
+ */
14
+ export type MessageRole = 'system' | 'user' | 'assistant';
15
+
16
+ /**
17
+ * A single message in a conversation
18
+ */
19
+ export interface Message {
20
+ role: MessageRole;
21
+ content: string;
22
+ }
23
+
24
+ /**
25
+ * Tool/function definition for function calling
26
+ */
27
+ export interface Tool {
28
+ type: 'function';
29
+ function: {
30
+ name: string;
31
+ description?: string;
32
+ parameters?: Record<string, unknown>;
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Configuration for a completion request
38
+ */
39
+ export interface CompletionRequest {
40
+ /** Model identifier (e.g., 'openai/gpt-4', 'ollama/mistral') */
41
+ model: string;
42
+
43
+ /** API key or authentication token */
44
+ apiKey?: string;
45
+
46
+ /** Base URL for the API (mainly for Ollama) */
47
+ baseUrl?: string;
48
+
49
+ /** Messages for the completion */
50
+ messages: Message[];
51
+
52
+ /** Maximum tokens to generate */
53
+ maxTokens?: number;
54
+
55
+ /** Sampling temperature (0-2 for OpenAI, typically 0-1) */
56
+ temperature?: number;
57
+
58
+ /** Top-p (nucleus sampling) */
59
+ topP?: number;
60
+
61
+ /** Top-k sampling parameter */
62
+ topK?: number;
63
+
64
+ /** Frequency penalty (-2 to 2 for OpenAI) */
65
+ frequencyPenalty?: number;
66
+
67
+ /** Presence penalty (-2 to 2 for OpenAI) */
68
+ presencePenalty?: number;
69
+
70
+ /** Stop sequences */
71
+ stop?: string[];
72
+
73
+ /** Available tools for function calling */
74
+ tools?: Tool[];
75
+
76
+ /** Force tool usage */
77
+ toolChoice?: 'auto' | 'required' | string;
78
+
79
+ /** Custom headers to send with requests */
80
+ headers?: Record<string, string>;
81
+
82
+ /** Request timeout in milliseconds */
83
+ timeout?: number;
84
+
85
+ /** Retry configuration */
86
+ retry?: {
87
+ maxRetries?: number;
88
+ backoffMultiplier?: number;
89
+ initialDelayMs?: number;
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Completion response
95
+ */
96
+ export interface CompletionResponse {
97
+ /** Generated text content */
98
+ content: string;
99
+
100
+ /** The model used */
101
+ model: string;
102
+
103
+ /** Stop reason */
104
+ stopReason?: 'stop_sequence' | 'length' | 'tool_calls' | 'end_turn' | string;
105
+
106
+ /** Total tokens used (if available) */
107
+ usage?: {
108
+ promptTokens?: number;
109
+ completionTokens?: number;
110
+ totalTokens?: number;
111
+ };
112
+
113
+ /** Raw provider response for advanced use cases */
114
+ raw?: unknown;
115
+
116
+ /** Finish reason from provider */
117
+ finishReason?: string;
118
+
119
+ /** Tool calls if function calling was used */
120
+ toolCalls?: Array<{
121
+ id?: string;
122
+ name: string;
123
+ arguments: Record<string, unknown>;
124
+ }>;
125
+ }
126
+
127
+ /**
128
+ * Streaming completion chunk
129
+ */
130
+ export interface CompletionChunk {
131
+ /** Delta content */
132
+ delta: string;
133
+
134
+ /** Stop reason if stream ended */
135
+ stopReason?: string;
136
+
137
+ /** Usage at end of stream */
138
+ usage?: {
139
+ promptTokens?: number;
140
+ completionTokens?: number;
141
+ totalTokens?: number;
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Provider configuration options
147
+ */
148
+ export interface ProviderConfig {
149
+ /** Provider type */
150
+ type: ProviderType;
151
+
152
+ /** API key */
153
+ apiKey?: string;
154
+
155
+ /** Base URL for API */
156
+ baseUrl?: string;
157
+
158
+ /** Default model */
159
+ model?: string;
160
+
161
+ /** Request timeout */
162
+ timeout?: number;
163
+
164
+ /** Retry configuration */
165
+ retry?: {
166
+ maxRetries?: number;
167
+ backoffMultiplier?: number;
168
+ initialDelayMs?: number;
169
+ };
170
+
171
+ /** Custom headers */
172
+ headers?: Record<string, string>;
173
+ }
174
+
175
+ /**
176
+ * Error response from provider
177
+ */
178
+ export interface ProviderError extends Error {
179
+ name: string;
180
+ message: string;
181
+ code?: string;
182
+ statusCode?: number;
183
+ details?: unknown;
184
+ retryable?: boolean;
185
+ }
186
+
187
+ /**
188
+ * Provider interface that all providers must implement
189
+ */
190
+ export interface IProvider {
191
+ /** Create a completion request */
192
+ complete(request: CompletionRequest): Promise<CompletionResponse>;
193
+
194
+ /** Stream a completion request */
195
+ completeStream(
196
+ request: CompletionRequest
197
+ ): AsyncIterable<CompletionChunk>;
198
+
199
+ /** Validate that the configuration is correct */
200
+ validate(): Promise<void>;
201
+
202
+ /** Parse model string (e.g., 'openai/gpt-4' -> 'gpt-4') */
203
+ parseModel(model: string): string;
204
+
205
+ /** Enable or disable debug mode */
206
+ setDebug(debug: boolean): void;
207
+
208
+ /** Set custom logger function */
209
+ setLogger(logger: (level: string, message: string, data?: unknown) => void): void;
210
+ }
211
+
212
+ /**
213
+ * Global completion options
214
+ */
215
+ export interface CompletionOptions {
216
+ /** Enable request logging for debugging */
217
+ debug?: boolean;
218
+
219
+ /** Custom logger function */
220
+ logger?: (level: string, message: string, data?: unknown) => void;
221
+
222
+ /** Global timeout override */
223
+ globalTimeout?: number;
224
+
225
+ /** Retry configuration override */
226
+ globalRetry?: {
227
+ maxRetries?: number;
228
+ backoffMultiplier?: number;
229
+ initialDelayMs?: number;
230
+ };
231
+ }