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.
@@ -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
- this.baseURL = config.baseURL || process.env.OPEN_ROUTER_BASE_URL || 'https://openrouter.ai/api/v1/chat/completions';
6
- this.apiKey = config.apiKey || process.env.OPEN_ROUTER_API_KEY;
7
- this.defaultModel = config.defaultModel || process.env.OPEN_ROUTER_DEFAULT_MODEL || 'openrouter/free';
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 || this.apiKey;
14
- const baseURL = requestOptions.baseURL || this.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: options.model || this.defaultModel,
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.6.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": {