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/CONFIG_README.md +98 -0
- package/README.md +382 -357
- package/cli.js +195 -0
- package/config.yaml +149 -0
- package/docs/BASIC_USAGE.md +296 -0
- package/docs/CLI.md +455 -0
- package/docs/GET_STARTED.md +129 -0
- package/docs/GUARDRAILS_GUIDE.md +734 -0
- package/docs/README.md +47 -0
- package/docs/ROUTER_GUIDE.md +397 -0
- package/docs/SERVER_MODE.md +350 -0
- package/index.js +199 -246
- package/package.json +43 -34
- package/providers/ollama.js +120 -88
- package/providers/openai.js +104 -0
- package/providers/openrouter.js +113 -79
- package/router.js +248 -0
- package/server.js +186 -0
- package/test.js +246 -0
- package/validate-config.js +87 -0
- package/LICENSE +0 -21
package/index.js
CHANGED
|
@@ -1,246 +1,199 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"type": "
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
},
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
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
|
+
}
|