llmjs2 1.6.1 → 1.7.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.
- package/core/providers/openrouter.js +102 -17
- package/package.json +4 -1
|
@@ -1,44 +1,123 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
2
|
|
|
3
|
+
function asNonEmptyString(value) {
|
|
4
|
+
if (typeof value !== 'string') {
|
|
5
|
+
return '';
|
|
6
|
+
}
|
|
7
|
+
return value.trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function firstNonEmptyString(...values) {
|
|
11
|
+
for (const value of values) {
|
|
12
|
+
const normalized = asNonEmptyString(value);
|
|
13
|
+
if (normalized) {
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isValidHttpUrl(value) {
|
|
21
|
+
const normalized = asNonEmptyString(value);
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(normalized);
|
|
28
|
+
return parsed.protocol === 'https:' || parsed.protocol === 'http:';
|
|
29
|
+
} catch (_error) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeOpenRouterModel(model) {
|
|
35
|
+
let normalized = asNonEmptyString(model);
|
|
36
|
+
|
|
37
|
+
if (!normalized) {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Only inspect the first provider separator and preserve the rest of the path.
|
|
42
|
+
const firstSlashIndex = normalized.indexOf('/');
|
|
43
|
+
if (firstSlashIndex === -1) {
|
|
44
|
+
return `openrouter/${normalized}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const provider = normalized.substring(0, firstSlashIndex).trim().toLowerCase();
|
|
48
|
+
if (provider === 'openrouter') {
|
|
49
|
+
const remainder = normalized.substring(firstSlashIndex + 1).trim();
|
|
50
|
+
|
|
51
|
+
if (!remainder) {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Keep "openrouter/free" and collapse "openrouter/openrouter/free" to "openrouter/free".
|
|
56
|
+
if (remainder.includes('/')) {
|
|
57
|
+
normalized = remainder;
|
|
58
|
+
} else {
|
|
59
|
+
normalized = `openrouter/${remainder}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return normalized;
|
|
64
|
+
}
|
|
65
|
+
|
|
3
66
|
class OpenRouterProvider {
|
|
4
67
|
constructor(config = {}) {
|
|
5
|
-
|
|
6
|
-
this.
|
|
7
|
-
|
|
68
|
+
const configuredBaseURL = firstNonEmptyString(config.baseURL, process.env.OPEN_ROUTER_BASE_URL);
|
|
69
|
+
this.baseURL = isValidHttpUrl(configuredBaseURL)
|
|
70
|
+
? configuredBaseURL
|
|
71
|
+
: 'https://openrouter.ai/api/v1/chat/completions';
|
|
72
|
+
|
|
73
|
+
this.apiKey = firstNonEmptyString(config.apiKey, process.env.OPEN_ROUTER_API_KEY);
|
|
74
|
+
const configuredDefaultModel = firstNonEmptyString(config.defaultModel, process.env.OPEN_ROUTER_DEFAULT_MODEL) || 'openrouter/openrouter/free';
|
|
75
|
+
this.defaultModel = configuredDefaultModel;
|
|
8
76
|
this.timeout = config.timeout || 60000; // 60 seconds
|
|
9
77
|
this.config = config; // Store entire config for additional properties like referer, title
|
|
10
78
|
}
|
|
11
79
|
|
|
12
80
|
async makeRequest(data, requestOptions = {}) {
|
|
13
|
-
const apiKey = requestOptions.apiKey
|
|
14
|
-
const baseURL = requestOptions.baseURL
|
|
81
|
+
const apiKey = firstNonEmptyString(requestOptions.apiKey, this.apiKey);
|
|
82
|
+
const baseURL = firstNonEmptyString(requestOptions.baseURL, this.baseURL);
|
|
15
83
|
const timeout = requestOptions.timeout || this.timeout;
|
|
84
|
+
const referer = firstNonEmptyString(requestOptions.referer, this.config.referer, process.env.OPEN_ROUTER_REFERER);
|
|
85
|
+
const title = firstNonEmptyString(requestOptions.title, this.config.title, process.env.OPEN_ROUTER_TITLE);
|
|
16
86
|
|
|
17
87
|
if (!apiKey) {
|
|
18
88
|
throw new Error('OpenRouter API key is required. Set OPEN_ROUTER_API_KEY environment variable or pass apiKey in config.');
|
|
19
89
|
}
|
|
20
90
|
|
|
91
|
+
if (!isValidHttpUrl(baseURL)) {
|
|
92
|
+
throw new Error('OpenRouter base URL is invalid. Set OPEN_ROUTER_BASE_URL to a valid http(s) URL.');
|
|
93
|
+
}
|
|
94
|
+
|
|
21
95
|
const postData = JSON.stringify(data);
|
|
22
96
|
|
|
23
97
|
const parsedUrl = new URL(baseURL);
|
|
24
98
|
|
|
99
|
+
const headers = {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
102
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (isValidHttpUrl(referer)) {
|
|
106
|
+
headers['HTTP-Referer'] = referer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (title) {
|
|
110
|
+
headers['X-Title'] = title;
|
|
111
|
+
}
|
|
112
|
+
|
|
25
113
|
const options = {
|
|
26
114
|
hostname: parsedUrl.hostname,
|
|
27
115
|
port: parsedUrl.port || 443,
|
|
28
116
|
path: parsedUrl.pathname + parsedUrl.search,
|
|
29
117
|
method: 'POST',
|
|
30
|
-
headers
|
|
31
|
-
'Content-Type': 'application/json',
|
|
32
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
33
|
-
'Content-Length': Buffer.byteLength(postData),
|
|
34
|
-
'HTTP-Referer': this.config.referer || process.env.OPEN_ROUTER_REFERER || '',
|
|
35
|
-
'X-Title': this.config.title || process.env.OPEN_ROUTER_TITLE || ''
|
|
36
|
-
}
|
|
118
|
+
headers
|
|
37
119
|
};
|
|
38
|
-
|
|
39
|
-
// Remove empty headers
|
|
40
|
-
if (!options.headers['HTTP-Referer']) delete options.headers['HTTP-Referer'];
|
|
41
|
-
if (!options.headers['X-Title']) delete options.headers['X-Title'];
|
|
120
|
+
|
|
42
121
|
|
|
43
122
|
return new Promise((resolve, reject) => {
|
|
44
123
|
const req = https.request(options, (res) => {
|
|
@@ -78,8 +157,14 @@ class OpenRouterProvider {
|
|
|
78
157
|
}
|
|
79
158
|
|
|
80
159
|
async createCompletion(messages, options = {}) {
|
|
160
|
+
const normalizedModel = normalizeOpenRouterModel(options.model || this.defaultModel);
|
|
161
|
+
|
|
162
|
+
if (!normalizedModel) {
|
|
163
|
+
throw new Error('OpenRouter model is invalid. Provide a non-empty model, for example "openrouter/free" or "meta-llama/llama-3.1-8b-instruct:free".');
|
|
164
|
+
}
|
|
165
|
+
|
|
81
166
|
const data = {
|
|
82
|
-
model:
|
|
167
|
+
model: normalizedModel,
|
|
83
168
|
messages: messages,
|
|
84
169
|
temperature: options.temperature || 0.7,
|
|
85
170
|
max_tokens: options.maxTokens,
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llmjs2",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "A unified Node.js library for connecting to multiple Large Language Model (LLM) providers: OpenAI, Ollama, and OpenRouter.",
|
|
5
5
|
"main": "core/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"llmjs2": "core/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"chat:router": "node core/examples/chat-router-sequential-app.js",
|
|
11
|
+
"chat:router:guardrail": "node core/examples/chat-router-guardrail.js",
|
|
10
12
|
"test": "node core/test.js",
|
|
11
13
|
"test:completion": "node core/test-completion.js",
|
|
12
14
|
"start": "node core/cli.js",
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
"url": ""
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
38
|
+
"@prompy/prompy": "^1.0.2",
|
|
36
39
|
"yaml": "^2.0.0"
|
|
37
40
|
},
|
|
38
41
|
"engines": {
|