llmjs2 1.7.1 → 2.0.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/README.md +116 -18
- package/dist/index.d.mts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +1242 -0
- package/dist/index.mjs +1211 -0
- package/package.json +32 -58
- package/chain/AGENT_STEP_README.md +0 -102
- package/chain/README.md +0 -257
- package/chain/WORKFLOW_README.md +0 -85
- package/chain/agent-step-example.js +0 -232
- package/chain/docs/AGENT.md +0 -126
- package/chain/docs/GRAPH.md +0 -490
- package/chain/examples.js +0 -314
- package/chain/index.js +0 -31
- package/chain/lib/agent.js +0 -338
- package/chain/lib/flow/agent-step.js +0 -119
- package/chain/lib/flow/edge.js +0 -24
- package/chain/lib/flow/flow.js +0 -76
- package/chain/lib/flow/graph.js +0 -331
- package/chain/lib/flow/index.js +0 -7
- package/chain/lib/flow/step.js +0 -63
- package/chain/lib/memory/in-memory.js +0 -117
- package/chain/lib/memory/index.js +0 -36
- package/chain/lib/memory/lance-memory.js +0 -225
- package/chain/lib/memory/sqlite-memory.js +0 -309
- package/chain/simple-agent-step-example.js +0 -168
- package/chain/workflow-example-usage.js +0 -70
- package/chain/workflow-example.json +0 -59
- package/core/README.md +0 -485
- package/core/cli.js +0 -275
- package/core/config.yaml +0 -149
- package/core/docs/BASIC_USAGE.md +0 -62
- package/core/docs/CLI.md +0 -104
- package/core/docs/GET_STARTED.md +0 -129
- package/core/docs/GUARDRAILS_GUIDE.md +0 -734
- package/core/docs/README.md +0 -47
- package/core/docs/ROUTER_GUIDE.md +0 -199
- package/core/docs/SERVER_MODE.md +0 -358
- package/core/index.js +0 -115
- package/core/logger.js +0 -115
- package/core/providers/ollama.js +0 -128
- package/core/providers/openai.js +0 -112
- package/core/providers/openrouter.js +0 -206
- package/core/router.js +0 -252
- package/core/server.js +0 -203
package/core/logger.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Simple logging utility for llmjs2
|
|
5
|
-
* Provides structured logging with timestamps and levels
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
class Logger {
|
|
9
|
-
constructor() {
|
|
10
|
-
this.levels = {
|
|
11
|
-
DEBUG: 0, // Shows all logs
|
|
12
|
-
INFO: 1, // Shows INFO and ERROR only
|
|
13
|
-
WARN: 2, // Shows WARN, INFO, and ERROR
|
|
14
|
-
ERROR: 3, // Shows ERROR only
|
|
15
|
-
FATAL: 4
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// Set level based on LLMJS2_LOG environment variable
|
|
19
|
-
this.setLevelFromEnv();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
setLevelFromEnv() {
|
|
23
|
-
const envLevel = process.env.LLMJS2_LOG;
|
|
24
|
-
if (!envLevel) {
|
|
25
|
-
// Default to no logging if not set
|
|
26
|
-
this.currentLevel = 99; // Higher than any level
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const upperEnvLevel = envLevel.toUpperCase();
|
|
31
|
-
if (this.levels[upperEnvLevel] !== undefined) {
|
|
32
|
-
this.currentLevel = this.levels[upperEnvLevel];
|
|
33
|
-
} else {
|
|
34
|
-
// Default to INFO if invalid value
|
|
35
|
-
this.currentLevel = this.levels.INFO;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
setLevel(level) {
|
|
40
|
-
if (this.levels[level] !== undefined) {
|
|
41
|
-
this.currentLevel = this.levels[level];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
formatMessage(level, message, data = null) {
|
|
46
|
-
const timestamp = new Date().toISOString();
|
|
47
|
-
let output = `[${timestamp}] [${level}] ${message}`;
|
|
48
|
-
|
|
49
|
-
if (data) {
|
|
50
|
-
output += '\n' + JSON.stringify(data, null, 2);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return output;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
shouldLog(level) {
|
|
57
|
-
const levelValue = this.levels[level];
|
|
58
|
-
|
|
59
|
-
// No logging if not configured
|
|
60
|
-
if (this.currentLevel === 99) return false;
|
|
61
|
-
|
|
62
|
-
// Custom logic based on user's requirements
|
|
63
|
-
if (this.currentLevel === this.levels.DEBUG) {
|
|
64
|
-
// Debug shows all levels
|
|
65
|
-
return true;
|
|
66
|
-
} else if (this.currentLevel === this.levels.INFO) {
|
|
67
|
-
// Info shows INFO and ERROR only
|
|
68
|
-
return level === 'INFO' || level === 'ERROR';
|
|
69
|
-
} else if (this.currentLevel === this.levels.WARN) {
|
|
70
|
-
// Warn shows WARN, INFO, and ERROR
|
|
71
|
-
return level === 'WARN' || level === 'INFO' || level === 'ERROR';
|
|
72
|
-
} else if (this.currentLevel === this.levels.ERROR) {
|
|
73
|
-
// Error shows ERROR only
|
|
74
|
-
return level === 'ERROR';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
debug(message, data = null) {
|
|
81
|
-
if (this.shouldLog('DEBUG')) {
|
|
82
|
-
console.log(this.formatMessage('DEBUG', message, data));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
info(message, data = null) {
|
|
87
|
-
if (this.shouldLog('INFO')) {
|
|
88
|
-
console.log(this.formatMessage('INFO', message, data));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
warn(message, data = null) {
|
|
93
|
-
if (this.shouldLog('WARN')) {
|
|
94
|
-
console.warn(this.formatMessage('WARN', message, data));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
error(message, data = null) {
|
|
99
|
-
if (this.shouldLog('ERROR')) {
|
|
100
|
-
console.error(this.formatMessage('ERROR', message, data));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
fatal(message, data = null) {
|
|
105
|
-
if (this.shouldLog('FATAL')) {
|
|
106
|
-
console.error(this.formatMessage('FATAL', message, data));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Create singleton logger instance
|
|
112
|
-
const logger = new Logger();
|
|
113
|
-
|
|
114
|
-
// Export for use in other modules
|
|
115
|
-
module.exports = logger;
|
package/core/providers/ollama.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
const https = require('https');
|
|
2
|
-
|
|
3
|
-
class OllamaProvider {
|
|
4
|
-
constructor(config = {}) {
|
|
5
|
-
this.baseURL = config.baseURL || process.env.OLLAMA_BASE_URL || 'https://ollama.com/api/chat';
|
|
6
|
-
this.apiKey = config.apiKey || process.env.OLLAMA_API_KEY;
|
|
7
|
-
this.defaultModel = config.defaultModel || process.env.OLLAMA_DEFAULT_MODEL || 'minimax-m2.5:cloud';
|
|
8
|
-
this.timeout = config.timeout || 120000; // 2 minutes (LLMs can be slow)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async makeRequest(data, requestOptions = {}) {
|
|
12
|
-
const apiKey = requestOptions.apiKey || this.apiKey;
|
|
13
|
-
const baseURL = requestOptions.baseURL || this.baseURL;
|
|
14
|
-
const timeout = requestOptions.timeout || this.timeout;
|
|
15
|
-
|
|
16
|
-
if (!apiKey) {
|
|
17
|
-
throw new Error('Ollama API key is required. Set OLLAMA_API_KEY environment variable or pass apiKey in config.');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const postData = JSON.stringify(data);
|
|
21
|
-
|
|
22
|
-
const parsedUrl = new URL(baseURL);
|
|
23
|
-
|
|
24
|
-
const options = {
|
|
25
|
-
hostname: parsedUrl.hostname,
|
|
26
|
-
port: parsedUrl.port || 443,
|
|
27
|
-
path: parsedUrl.pathname + parsedUrl.search,
|
|
28
|
-
method: 'POST',
|
|
29
|
-
headers: {
|
|
30
|
-
'Content-Type': 'application/json',
|
|
31
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
32
|
-
'Content-Length': Buffer.byteLength(postData)
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return new Promise((resolve, reject) => {
|
|
37
|
-
const req = https.request(options, (res) => {
|
|
38
|
-
let body = '';
|
|
39
|
-
|
|
40
|
-
res.on('data', (chunk) => {
|
|
41
|
-
body += chunk;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
res.on('end', () => {
|
|
45
|
-
try {
|
|
46
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
47
|
-
const response = JSON.parse(body);
|
|
48
|
-
resolve(response);
|
|
49
|
-
} else {
|
|
50
|
-
const error = JSON.parse(body);
|
|
51
|
-
reject(new Error(`Ollama API error (${res.statusCode}): ${error.error?.message || error.message || body }`));
|
|
52
|
-
}
|
|
53
|
-
} catch (parseError) {
|
|
54
|
-
reject(new Error(`Failed to parse Ollama response: ${parseError.message}`));
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
req.on('error', (error) => {
|
|
60
|
-
reject(new Error(`Ollama request failed: ${error.message}`));
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
req.setTimeout(timeout, () => {
|
|
64
|
-
req.destroy();
|
|
65
|
-
reject(new Error('Ollama request timed out'));
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
req.write(postData);
|
|
69
|
-
req.end();
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async createCompletion(messages, options = {}) {
|
|
74
|
-
const data = {
|
|
75
|
-
model: options.model || this.defaultModel,
|
|
76
|
-
messages: messages,
|
|
77
|
-
stream: false, // We handle non-streaming for simplicity
|
|
78
|
-
format: options.format,
|
|
79
|
-
options: {
|
|
80
|
-
temperature: options.temperature || 0.7,
|
|
81
|
-
top_p: options.topP,
|
|
82
|
-
num_predict: options.maxTokens,
|
|
83
|
-
repeat_penalty: options.frequencyPenalty,
|
|
84
|
-
presence_penalty: options.presencePenalty,
|
|
85
|
-
stop: options.stop,
|
|
86
|
-
num_ctx: options.numCtx || 4096
|
|
87
|
-
},
|
|
88
|
-
tools: options.tools,
|
|
89
|
-
keep_alive: options.keepAlive || '5m'
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Remove undefined values
|
|
93
|
-
Object.keys(data).forEach(key => {
|
|
94
|
-
if (data[key] === undefined) {
|
|
95
|
-
delete data[key];
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
if (data.options) {
|
|
100
|
-
Object.keys(data.options).forEach(key => {
|
|
101
|
-
if (data.options[key] === undefined) {
|
|
102
|
-
delete data.options[key];
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const response = await this.makeRequest(data, {
|
|
108
|
-
apiKey: options.apiKey,
|
|
109
|
-
baseURL: options.baseURL,
|
|
110
|
-
timeout: options.timeout
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
content: response.message?.content || '',
|
|
115
|
-
role: response.message?.role || 'assistant',
|
|
116
|
-
tool_calls: response.message?.tool_calls,
|
|
117
|
-
usage: {
|
|
118
|
-
prompt_eval_count: response.prompt_eval_count,
|
|
119
|
-
eval_count: response.eval_count,
|
|
120
|
-
total_duration: response.total_duration
|
|
121
|
-
},
|
|
122
|
-
model: response.model,
|
|
123
|
-
finishReason: response.done_reason || (response.done ? 'stop' : null)
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = OllamaProvider;
|
package/core/providers/openai.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
const https = require('https');
|
|
2
|
-
|
|
3
|
-
class OpenAIProvider {
|
|
4
|
-
constructor(config = {}) {
|
|
5
|
-
this.baseURL = config.baseURL || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
|
6
|
-
this.apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
|
7
|
-
this.defaultModel = config.defaultModel || process.env.OPENAI_DEFAULT_MODEL || 'gpt-3.5-turbo';
|
|
8
|
-
this.timeout = config.timeout || 60000; // 60 seconds
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async makeRequest(endpoint, data, requestOptions = {}) {
|
|
12
|
-
const apiKey = requestOptions.apiKey || this.apiKey;
|
|
13
|
-
const baseURL = requestOptions.baseURL || this.baseURL;
|
|
14
|
-
const timeout = requestOptions.timeout || this.timeout;
|
|
15
|
-
|
|
16
|
-
if (!apiKey) {
|
|
17
|
-
throw new Error('OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass apiKey in config.');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const requestUrl = `${baseURL}${endpoint}`;
|
|
21
|
-
const postData = JSON.stringify(data);
|
|
22
|
-
|
|
23
|
-
const parsedUrl = new URL(requestUrl);
|
|
24
|
-
|
|
25
|
-
const options = {
|
|
26
|
-
hostname: parsedUrl.hostname,
|
|
27
|
-
port: parsedUrl.port || 443,
|
|
28
|
-
path: parsedUrl.pathname + parsedUrl.search,
|
|
29
|
-
method: 'POST',
|
|
30
|
-
headers: {
|
|
31
|
-
'Content-Type': 'application/json',
|
|
32
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
33
|
-
'Content-Length': Buffer.byteLength(postData)
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return new Promise((resolve, reject) => {
|
|
38
|
-
const req = https.request(options, (res) => {
|
|
39
|
-
let body = '';
|
|
40
|
-
|
|
41
|
-
res.on('data', (chunk) => {
|
|
42
|
-
body += chunk;
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
res.on('end', () => {
|
|
46
|
-
try {
|
|
47
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
48
|
-
const response = JSON.parse(body);
|
|
49
|
-
resolve(response);
|
|
50
|
-
} else {
|
|
51
|
-
const error = JSON.parse(body);
|
|
52
|
-
reject(new Error(`OpenAI API error (${res.statusCode}): ${error.error?.message || 'Unknown error'}`));
|
|
53
|
-
}
|
|
54
|
-
} catch (parseError) {
|
|
55
|
-
reject(new Error(`Failed to parse OpenAI response: ${parseError.message}`));
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
req.on('error', (error) => {
|
|
61
|
-
reject(new Error(`OpenAI request failed: ${error.message}`));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
req.setTimeout(timeout, () => {
|
|
65
|
-
req.destroy();
|
|
66
|
-
reject(new Error('OpenAI request timed out'));
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
req.write(postData);
|
|
70
|
-
req.end();
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async createCompletion(messages, options = {}) {
|
|
75
|
-
const data = {
|
|
76
|
-
model: options.model || this.defaultModel,
|
|
77
|
-
messages: messages,
|
|
78
|
-
temperature: options.temperature || 0.7,
|
|
79
|
-
max_tokens: options.maxTokens,
|
|
80
|
-
top_p: options.topP,
|
|
81
|
-
frequency_penalty: options.frequencyPenalty,
|
|
82
|
-
presence_penalty: options.presencePenalty,
|
|
83
|
-
stop: options.stop,
|
|
84
|
-
tools: options.tools,
|
|
85
|
-
tool_choice: options.toolChoice
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// Remove undefined values
|
|
89
|
-
Object.keys(data).forEach(key => {
|
|
90
|
-
if (data[key] === undefined) {
|
|
91
|
-
delete data[key];
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const response = await this.makeRequest('/chat/completions', data, {
|
|
96
|
-
apiKey: options.apiKey,
|
|
97
|
-
baseURL: options.baseURL,
|
|
98
|
-
timeout: options.timeout
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
content: response.choices[0]?.message?.content || '',
|
|
103
|
-
role: response.choices[0]?.message?.role || 'assistant',
|
|
104
|
-
tool_calls: response.choices[0]?.message?.tool_calls,
|
|
105
|
-
usage: response.usage,
|
|
106
|
-
model: response.model,
|
|
107
|
-
finishReason: response.choices[0]?.finish_reason
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
module.exports = OpenAIProvider;
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
const https = require('https');
|
|
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
|
-
|
|
66
|
-
class OpenRouterProvider {
|
|
67
|
-
constructor(config = {}) {
|
|
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;
|
|
76
|
-
this.timeout = config.timeout || 60000; // 60 seconds
|
|
77
|
-
this.config = config; // Store entire config for additional properties like referer, title
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async makeRequest(data, requestOptions = {}) {
|
|
81
|
-
const apiKey = firstNonEmptyString(requestOptions.apiKey, this.apiKey);
|
|
82
|
-
const baseURL = firstNonEmptyString(requestOptions.baseURL, this.baseURL);
|
|
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);
|
|
86
|
-
|
|
87
|
-
if (!apiKey) {
|
|
88
|
-
throw new Error('OpenRouter API key is required. Set OPEN_ROUTER_API_KEY environment variable or pass apiKey in config.');
|
|
89
|
-
}
|
|
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
|
-
|
|
95
|
-
const postData = JSON.stringify(data);
|
|
96
|
-
|
|
97
|
-
const parsedUrl = new URL(baseURL);
|
|
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
|
-
|
|
113
|
-
const options = {
|
|
114
|
-
hostname: parsedUrl.hostname,
|
|
115
|
-
port: parsedUrl.port || 443,
|
|
116
|
-
path: parsedUrl.pathname + parsedUrl.search,
|
|
117
|
-
method: 'POST',
|
|
118
|
-
headers
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return new Promise((resolve, reject) => {
|
|
123
|
-
const req = https.request(options, (res) => {
|
|
124
|
-
let body = '';
|
|
125
|
-
|
|
126
|
-
res.on('data', (chunk) => {
|
|
127
|
-
body += chunk;
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
res.on('end', () => {
|
|
131
|
-
try {
|
|
132
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
133
|
-
const response = JSON.parse(body);
|
|
134
|
-
resolve(response);
|
|
135
|
-
} else {
|
|
136
|
-
const error = JSON.parse(body);
|
|
137
|
-
reject(new Error(`OpenRouter API error (${res.statusCode}): ${error.error?.message || 'Unknown error'}`));
|
|
138
|
-
}
|
|
139
|
-
} catch (parseError) {
|
|
140
|
-
reject(new Error(`Failed to parse OpenRouter response: ${parseError.message}`));
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
req.on('error', (error) => {
|
|
146
|
-
reject(new Error(`OpenRouter request failed: ${error.message}`));
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
req.setTimeout(timeout, () => {
|
|
150
|
-
req.destroy();
|
|
151
|
-
reject(new Error('OpenRouter request timed out'));
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
req.write(postData);
|
|
155
|
-
req.end();
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
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
|
-
|
|
166
|
-
const data = {
|
|
167
|
-
model: normalizedModel,
|
|
168
|
-
messages: messages,
|
|
169
|
-
temperature: options.temperature || 0.7,
|
|
170
|
-
max_tokens: options.maxTokens,
|
|
171
|
-
top_p: options.topP,
|
|
172
|
-
frequency_penalty: options.frequencyPenalty,
|
|
173
|
-
presence_penalty: options.presencePenalty,
|
|
174
|
-
stop: options.stop,
|
|
175
|
-
tools: options.tools,
|
|
176
|
-
tool_choice: options.toolChoice,
|
|
177
|
-
transforms: options.transforms,
|
|
178
|
-
models: options.models,
|
|
179
|
-
route: options.route
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// Remove undefined values
|
|
183
|
-
Object.keys(data).forEach(key => {
|
|
184
|
-
if (data[key] === undefined) {
|
|
185
|
-
delete data[key];
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
const response = await this.makeRequest(data, {
|
|
190
|
-
apiKey: options.apiKey,
|
|
191
|
-
baseURL: options.baseURL,
|
|
192
|
-
timeout: options.timeout
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
content: response.choices[0]?.message?.content || '',
|
|
197
|
-
role: response.choices[0]?.message?.role || 'assistant',
|
|
198
|
-
tool_calls: response.choices[0]?.message?.tool_calls,
|
|
199
|
-
usage: response.usage,
|
|
200
|
-
model: response.model,
|
|
201
|
-
finishReason: response.choices[0]?.finish_reason
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
module.exports = OpenRouterProvider;
|