llmjs2 1.1.1 → 1.3.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/index.js CHANGED
@@ -1,246 +1,199 @@
1
- import { ollamaCompletion } from './providers/ollama.js';
2
- import { openrouterCompletion } from './providers/openrouter.js';
3
-
4
- /**
5
- * Main completion function that routes to the appropriate provider
6
- * Supports three calling conventions:
7
- * 1. completion(prompt) or completion(model, prompt) - Simple API with auto-detection
8
- * 2. completion(model, prompt, apiKey) - Function-based API
9
- * 3. completion({ model, messages, apiKey, tools }) - Object-based API
10
- *
11
- * @param {string|object} modelOrOptions - Model identifier, prompt, or options object
12
- * @param {string} [prompt] - The prompt to send (for function-based API)
13
- * @param {string} [apiKey] - Optional API key (falls back to environment variables)
14
- * @returns {Promise<string|object>} The completion result (string or object with tool calls)
15
- */
16
- export async function completion(modelOrOptions, prompt, apiKey) {
17
- let model, messages, key, tools;
18
-
19
- // Handle simple API: completion(prompt) or completion(model, prompt)
20
- if (typeof modelOrOptions === 'string' && (prompt === undefined || typeof prompt === 'string')) {
21
- let simplePrompt, simpleModel;
22
-
23
- // If only one argument, determine if it's a prompt or a model
24
- if (prompt === undefined) {
25
- const trimmedModel = modelOrOptions.trim();
26
- // Check if it looks like a model (contains /)
27
- if (trimmedModel.includes('/')) {
28
- // Function API: completion(model) - missing prompt
29
- throw new Error('Prompt parameter is required');
30
- } else {
31
- // Simple API: completion(prompt)
32
- simplePrompt = trimmedModel;
33
- simpleModel = null;
34
- }
35
- }
36
- // If two arguments, first is model, second is prompt
37
- else {
38
- simpleModel = modelOrOptions.trim();
39
- simplePrompt = prompt.trim();
40
- }
41
-
42
- if (!simplePrompt) {
43
- throw new Error('Prompt parameter cannot be empty');
44
- }
45
-
46
- // Validate model if provided
47
- if (simpleModel !== null && simpleModel !== undefined) {
48
- const slashIndex = simpleModel.indexOf('/');
49
- if (slashIndex === -1) {
50
- throw new Error('Model must be in format "provider/model_name" (e.g., "ollama/llama2")');
51
- }
52
-
53
- const modelProvider = simpleModel.substring(0, slashIndex).trim();
54
- const modelName = simpleModel.substring(slashIndex + 1).trim();
55
-
56
- if (!modelProvider || !modelName) {
57
- throw new Error('Model must be in format "provider/model_name" (e.g., "ollama/llama2")');
58
- }
59
-
60
- if (modelProvider !== 'ollama' && modelProvider !== 'openrouter') {
61
- throw new Error(`Unsupported provider: ${modelProvider}. Supported providers: ollama, openrouter`);
62
- }
63
- }
64
-
65
- messages = [
66
- {
67
- role: 'user',
68
- content: simplePrompt
69
- }
70
- ];
71
-
72
- // Determine which provider to use
73
- let provider;
74
- if (simpleModel) {
75
- // If model is provided, use the provider from the model
76
- const slashIndex = simpleModel.indexOf('/');
77
- if (slashIndex === -1) {
78
- throw new Error('Model must be in format "provider/model_name" (e.g., "ollama/llama2")');
79
- }
80
-
81
- const modelProvider = simpleModel.substring(0, slashIndex).trim();
82
- const modelName = simpleModel.substring(slashIndex + 1).trim();
83
-
84
- if (!modelProvider || !modelName) {
85
- throw new Error('Model must be in format "provider/model_name" (e.g., "ollama/llama2")');
86
- }
87
-
88
- provider = modelProvider.toLowerCase();
89
- model = modelName;
90
-
91
- // Use the API key from the model's provider (may be undefined)
92
- if (provider === 'ollama') {
93
- key = process.env.OLLAMA_API_KEY;
94
- } else if (provider === 'openrouter') {
95
- key = process.env.OPEN_ROUTER_API_KEY;
96
- }
97
- } else {
98
- // No model provided - use default based on available keys
99
- const hasOllamaKey = !!process.env.OLLAMA_API_KEY;
100
- const hasOpenRouterKey = !!process.env.OPEN_ROUTER_API_KEY;
101
-
102
- if (!hasOllamaKey && !hasOpenRouterKey) {
103
- throw new Error('No API key found. Set OLLAMA_API_KEY or OPEN_ROUTER_API_KEY environment variable.');
104
- }
105
-
106
- if (hasOllamaKey && hasOpenRouterKey) {
107
- // Randomly choose between Ollama and OpenRouter
108
- provider = Math.random() < 0.5 ? 'ollama' : 'openrouter';
109
- } else if (hasOllamaKey) {
110
- provider = 'ollama';
111
- } else {
112
- provider = 'openrouter';
113
- }
114
-
115
- // Use default model for the provider
116
- if (provider === 'ollama') {
117
- model = process.env.OLLAMA_DEFAULT_MODEL || 'minimax-m2.5:cloud';
118
- key = process.env.OLLAMA_API_KEY;
119
- } else {
120
- model = process.env.OPEN_ROUTER_DEFAULT_MODEL || 'openrouter/free';
121
- key = process.env.OPEN_ROUTER_API_KEY;
122
- }
123
- }
124
-
125
- // Call the appropriate provider
126
- if (provider === 'ollama') {
127
- return ollamaCompletion(model, messages, key);
128
- } else {
129
- return openrouterCompletion(model, messages, key);
130
- }
131
- }
132
-
133
- // Handle object-based API
134
- if (typeof modelOrOptions === 'object' && modelOrOptions !== null) {
135
- const options = modelOrOptions;
136
- model = options.model;
137
- messages = options.messages;
138
- key = options.apiKey;
139
- tools = options.tools;
140
-
141
- if (!model || typeof model !== 'string') {
142
- throw new Error('Model parameter is required and must be a string');
143
- }
144
-
145
- if (!messages || !Array.isArray(messages) || messages.length === 0) {
146
- throw new Error('Messages parameter is required and must be a non-empty array');
147
- }
148
-
149
- // Validate messages format
150
- for (const msg of messages) {
151
- if (!msg.role) {
152
- throw new Error('Each message must have a "role" property');
153
- }
154
-
155
- // Tool messages have role='tool' and tool_call_id instead of content
156
- if (msg.role === 'tool') {
157
- if (!msg.tool_call_id && !msg.name) {
158
- throw new Error('Tool message must have "tool_call_id" and "name" properties');
159
- }
160
- if (msg.content !== undefined && typeof msg.content !== 'string') {
161
- throw new Error('Tool message "content" must be a string');
162
- }
163
- } else {
164
- // Non-tool messages must have content
165
- // Assistant messages with tool_calls can have empty content
166
- const hasContent = msg.content && msg.content.trim().length > 0;
167
- const isAssistantWithToolCalls = msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0;
168
- if (!hasContent && !isAssistantWithToolCalls) {
169
- throw new Error(`Message with role "${msg.role}" must have "content" property`);
170
- }
171
- if (!['system', 'user', 'assistant', 'tool'].includes(msg.role)) {
172
- throw new Error(`Message role must be "system", "user", "assistant", or "tool"`);
173
- }
174
- }
175
- }
176
-
177
- // Validate tools format if provided
178
- if (tools !== undefined) {
179
- if (!Array.isArray(tools)) {
180
- throw new Error('Tools parameter must be an array');
181
- }
182
- for (const tool of tools) {
183
- if (!tool.type || tool.type !== 'function') {
184
- throw new Error('Each tool must have type "function"');
185
- }
186
- if (!tool.function || !tool.function.name) {
187
- throw new Error('Each tool must have a function with a name');
188
- }
189
- }
190
- }
191
- }
192
- // Handle function-based API
193
- else {
194
- model = modelOrOptions;
195
-
196
- if (!model || typeof model !== 'string') {
197
- throw new Error('Model parameter is required and must be a string');
198
- }
199
-
200
- if (!prompt || typeof prompt !== 'string') {
201
- throw new Error('Prompt parameter is required and must be a string');
202
- }
203
-
204
- const trimmedPrompt = prompt.trim();
205
- if (!trimmedPrompt) {
206
- throw new Error('Prompt parameter cannot be empty');
207
- }
208
-
209
- messages = [
210
- {
211
- role: 'user',
212
- content: trimmedPrompt
213
- }
214
- ];
215
-
216
- key = apiKey;
217
- }
218
-
219
- const trimmedModel = model.trim();
220
- if (!trimmedModel) {
221
- throw new Error('Model parameter cannot be empty');
222
- }
223
-
224
- const slashIndex = trimmedModel.indexOf('/');
225
- if (slashIndex === -1) {
226
- throw new Error('Model must be in format "provider/model_name" (e.g., "ollama/llama2")');
227
- }
228
-
229
- const provider = trimmedModel.substring(0, slashIndex).trim();
230
- const modelName = trimmedModel.substring(slashIndex + 1).trim();
231
-
232
- if (!provider || !modelName) {
233
- throw new Error('Model must be in format "provider/model_name" (e.g., "ollama/llama2")');
234
- }
235
-
236
- const normalizedProvider = provider.toLowerCase();
237
-
238
- switch (normalizedProvider) {
239
- case 'ollama':
240
- return ollamaCompletion(modelName, messages, key, tools);
241
- case 'openrouter':
242
- return openrouterCompletion(modelName, messages, key, tools);
243
- default:
244
- throw new Error(`Unsupported provider: ${provider}. Supported providers: ollama, openrouter`);
245
- }
246
- }
1
+ const OpenAIProvider = require('./providers/openai');
2
+ const OllamaProvider = require('./providers/ollama');
3
+ const OpenRouterProvider = require('./providers/openrouter');
4
+ const { router } = require('./router');
5
+ const { app } = require('./server');
6
+
7
+ class LLMJS2 {
8
+ constructor(config = {}) {
9
+ this.providers = {
10
+ openai: new OpenAIProvider(config.openai || {}),
11
+ ollama: new OllamaProvider(config.ollama || {}),
12
+ openrouter: new OpenRouterProvider(config.openrouter || {})
13
+ };
14
+ this.defaultProvider = config.defaultProvider;
15
+ this.timeout = config.timeout || 60000;
16
+ }
17
+
18
+ /**
19
+ * Get available providers based on API keys
20
+ */
21
+ getAvailableProviders() {
22
+ const available = [];
23
+
24
+ const openaiKey = process.env.OPENAI_API_KEY || this.providers.openai.apiKey;
25
+ const ollamaKey = process.env.OLLAMA_API_KEY || this.providers.ollama.apiKey;
26
+ const openrouterKey = process.env.OPEN_ROUTER_API_KEY || this.providers.openrouter.apiKey;
27
+
28
+ // Check if keys are non-empty and not placeholder values
29
+ if (openaiKey && typeof openaiKey === 'string' && openaiKey.trim() && !openaiKey.startsWith(':')) {
30
+ available.push('openai');
31
+ }
32
+ if (ollamaKey && typeof ollamaKey === 'string' && ollamaKey.trim() && !ollamaKey.startsWith(':')) {
33
+ available.push('ollama');
34
+ }
35
+ if (openrouterKey && typeof openrouterKey === 'string' && openrouterKey.trim() && !openrouterKey.startsWith(':')) {
36
+ available.push('openrouter');
37
+ }
38
+
39
+ return available;
40
+ }
41
+
42
+ /**
43
+ * Parse model string like 'provider/model_name' or just 'model_name'
44
+ * Only splits on the first '/', since model names can contain '/' characters
45
+ */
46
+ parseModel(modelString) {
47
+ if (!modelString || typeof modelString !== 'string') {
48
+ return { provider: null, model: null };
49
+ }
50
+
51
+ const firstSlashIndex = modelString.indexOf('/');
52
+ if (firstSlashIndex !== -1) {
53
+ return {
54
+ provider: modelString.substring(0, firstSlashIndex),
55
+ model: modelString.substring(firstSlashIndex + 1)
56
+ };
57
+ }
58
+
59
+ return { provider: null, model: modelString };
60
+ }
61
+
62
+ /**
63
+ * Determine which provider to use
64
+ */
65
+ getProvider(modelString, options = {}) {
66
+ const { provider: specifiedProvider, model } = this.parseModel(modelString);
67
+
68
+ if (specifiedProvider) {
69
+ if (!this.providers[specifiedProvider]) {
70
+ throw new Error(`Unknown provider: ${specifiedProvider}`);
71
+ }
72
+ return { provider: this.providers[specifiedProvider], model };
73
+ }
74
+
75
+ // Auto-detect provider
76
+ const availableProviders = this.getAvailableProviders();
77
+
78
+ if (availableProviders.length === 0) {
79
+ throw new Error('No API keys configured. Set OPENAI_API_KEY, OLLAMA_API_KEY, or OPEN_ROUTER_API_KEY environment variables.');
80
+ }
81
+
82
+ // Use default provider if specified, otherwise use first available
83
+ const providerName = this.defaultProvider || availableProviders[0];
84
+ const provider = this.providers[providerName];
85
+
86
+ if (!provider) {
87
+ throw new Error(`Provider ${providerName} is not available`);
88
+ }
89
+
90
+ return { provider, model: model || provider.defaultModel };
91
+ }
92
+
93
+ /**
94
+ * Validate input parameters
95
+ */
96
+ validateInput(input) {
97
+ if (typeof input === 'string') {
98
+ // Simple string prompt
99
+ if (!input.trim()) {
100
+ throw new Error('Prompt cannot be empty');
101
+ }
102
+ return {
103
+ model: null,
104
+ messages: [{ role: 'user', content: input }],
105
+ options: {}
106
+ };
107
+ }
108
+
109
+ if (typeof input === 'object' && input !== null) {
110
+ // Object-based API
111
+ if (!input.messages || !Array.isArray(input.messages)) {
112
+ throw new Error('messages must be an array');
113
+ }
114
+
115
+ if (input.messages.length === 0) {
116
+ throw new Error('messages array cannot be empty');
117
+ }
118
+
119
+ // Validate message format
120
+ for (const msg of input.messages) {
121
+ if (!msg.role || !msg.content) {
122
+ throw new Error('Each message must have role and content properties');
123
+ }
124
+ if (!['system', 'user', 'assistant'].includes(msg.role)) {
125
+ throw new Error('Message role must be system, user, or assistant');
126
+ }
127
+ }
128
+
129
+ return {
130
+ model: input.model,
131
+ messages: input.messages,
132
+ options: {
133
+ temperature: input.temperature,
134
+ maxTokens: input.max_tokens || input.maxTokens,
135
+ topP: input.top_p || input.topP,
136
+ frequencyPenalty: input.frequency_penalty || input.frequencyPenalty,
137
+ presencePenalty: input.presence_penalty || input.presencePenalty,
138
+ stop: input.stop,
139
+ tools: input.tools,
140
+ toolChoice: input.tool_choice || input.toolChoice,
141
+ apiKey: input.apiKey,
142
+ timeout: input.timeout
143
+ }
144
+ };
145
+ }
146
+
147
+ throw new Error('Input must be a string or object with messages');
148
+ }
149
+
150
+ /**
151
+ * Main completion function
152
+ */
153
+ async completion(input) {
154
+ try {
155
+ const { model, messages, options } = this.validateInput(input);
156
+
157
+ const { provider, model: finalModel } = this.getProvider(model, options);
158
+
159
+ // Override provider API key if specified in options
160
+ if (options.apiKey) {
161
+ provider.apiKey = options.apiKey;
162
+ }
163
+
164
+ // Override timeout if specified
165
+ if (options.timeout) {
166
+ provider.timeout = options.timeout;
167
+ }
168
+
169
+ const result = await provider.createCompletion(messages, { ...options, model: finalModel });
170
+
171
+ return result.content;
172
+
173
+ } catch (error) {
174
+ // Sanitize error message to avoid leaking sensitive information
175
+ const message = error.message || 'Unknown error occurred';
176
+
177
+ // Don't include API keys or other sensitive data in error messages
178
+ const sanitizedMessage = message.replace(/Bearer\s+[^\s]+/gi, 'Bearer [REDACTED]');
179
+
180
+ throw new Error(`LLMJS2 completion failed: ${sanitizedMessage}`);
181
+ }
182
+ }
183
+ }
184
+
185
+ // Export the completion function directly for convenience
186
+ function completion(input) {
187
+ const llm = new LLMJS2();
188
+ return llm.completion(input);
189
+ }
190
+
191
+ module.exports = {
192
+ completion,
193
+ LLMJS2,
194
+ router,
195
+ app,
196
+ OpenAIProvider,
197
+ OllamaProvider,
198
+ OpenRouterProvider
199
+ };
package/package.json CHANGED
@@ -1,34 +1,43 @@
1
- {
2
- "name": "llmjs2",
3
- "version": "1.1.1",
4
- "description": "Abstract layer for LLM completion supporting multiple providers",
5
- "main": "index.js",
6
- "type": "module",
7
- "exports": {
8
- ".": "./index.js"
9
- },
10
- "files": [
11
- "index.js",
12
- "providers/",
13
- "README.md",
14
- "LICENSE"
15
- ],
16
- "keywords": [
17
- "llm",
18
- "ai",
19
- "completion",
20
- "llmjs2",
21
- "ollama",
22
- "openrouter",
23
- "api"
24
- ],
25
- "author": "",
26
- "license": "MIT",
27
- "engines": {
28
- "node": ">=14.0.0"
29
- },
30
- "repository": {
31
- "type": "git",
32
- "url": ""
33
- }
34
- }
1
+ {
2
+ "name": "llmjs2",
3
+ "version": "1.3.0",
4
+ "description": "A unified Node.js library for connecting to multiple LLM providers: OpenAI, Ollama, and OpenRouter",
5
+ "main": "index.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "test": "node test.js",
9
+ "start": "node cli.js",
10
+ "server": "node cli.js",
11
+ "lint": "echo 'No linting configured'",
12
+ "typecheck": "echo 'No TypeScript configured'"
13
+ },
14
+ "keywords": [
15
+ "llm",
16
+ "openai",
17
+ "ollama",
18
+ "openrouter",
19
+ "ai",
20
+ "chatgpt",
21
+ "completions",
22
+ "unified-api"
23
+ ],
24
+ "author": "Your Name",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/littlellmjs/llmjs2.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/littlellmjs/llmjs2/issues"
32
+ },
33
+ "homepage": "https://github.com/littlellmjs/llmjs2#readme",
34
+ "engines": {
35
+ "node": ">=14.0.0"
36
+ },
37
+ "dependencies": {
38
+ "yaml": "^2.3.4"
39
+ },
40
+ "bin": {
41
+ "llmjs2": "./cli.js"
42
+ }
43
+ }