converse-mcp-server 2.3.1 → 2.4.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 +771 -738
- package/docs/API.md +10 -1
- package/docs/PROVIDERS.md +8 -4
- package/package.json +12 -12
- package/src/async/asyncJobStore.js +82 -52
- package/src/async/eventBus.js +25 -20
- package/src/async/fileCache.js +121 -40
- package/src/async/jobRunner.js +65 -39
- package/src/async/providerStreamNormalizer.js +203 -117
- package/src/config.js +374 -102
- package/src/continuationStore.js +32 -24
- package/src/index.js +45 -25
- package/src/prompts/helpPrompt.js +328 -305
- package/src/providers/anthropic.js +303 -119
- package/src/providers/codex.js +103 -45
- package/src/providers/deepseek.js +24 -8
- package/src/providers/google.js +337 -93
- package/src/providers/index.js +1 -1
- package/src/providers/interface.js +16 -11
- package/src/providers/mistral.js +179 -69
- package/src/providers/openai-compatible.js +231 -94
- package/src/providers/openai.js +1094 -914
- package/src/providers/openrouter-endpoints-client.js +220 -216
- package/src/providers/openrouter.js +426 -381
- package/src/providers/xai.js +153 -56
- package/src/resources/helpResource.js +70 -67
- package/src/router.js +95 -67
- package/src/services/summarizationService.js +51 -24
- package/src/systemPrompts.js +89 -89
- package/src/tools/cancelJob.js +31 -19
- package/src/tools/chat.js +997 -883
- package/src/tools/checkStatus.js +86 -65
- package/src/tools/consensus.js +400 -234
- package/src/tools/index.js +39 -16
- package/src/transport/httpTransport.js +82 -55
- package/src/utils/contextProcessor.js +54 -37
- package/src/utils/errorHandler.js +95 -45
- package/src/utils/fileValidator.js +107 -98
- package/src/utils/formatStatus.js +122 -64
- package/src/utils/logger.js +459 -449
- package/src/utils/pathUtils.js +2 -2
- package/src/utils/tokenLimiter.js +216 -216
|
@@ -1,381 +1,426 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenRouter Provider
|
|
3
|
-
*
|
|
4
|
-
* Provider implementation for OpenRouter's unified API gateway using OpenAI-compatible API.
|
|
5
|
-
* Implements the unified interface: async invoke(messages, options) => { content, stop_reason, rawResponse }
|
|
6
|
-
*
|
|
7
|
-
* OpenRouter provides access to multiple AI models through a single API endpoint.
|
|
8
|
-
* IMPORTANT: Requires HTTP-Referer header for compliance tracking.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createOpenAICompatibleProvider } from './openai-compatible.js';
|
|
12
|
-
import { debugLog } from '../utils/console.js';
|
|
13
|
-
import { ProviderError, ErrorCodes } from './interface.js';
|
|
14
|
-
import { fetchModelEndpointsWithCache } from './openrouter-endpoints-client.js';
|
|
15
|
-
|
|
16
|
-
// Define supported OpenRouter models with their capabilities
|
|
17
|
-
// Only including the three specific models requested
|
|
18
|
-
const SUPPORTED_MODELS = {
|
|
19
|
-
'qwen/qwen3-235b-a22b-thinking-2507': {
|
|
20
|
-
modelName: 'qwen/qwen3-235b-a22b-thinking-2507',
|
|
21
|
-
friendlyName: 'Qwen3 235B Thinking (via OpenRouter)',
|
|
22
|
-
contextWindow: 32768,
|
|
23
|
-
maxOutputTokens: 8192,
|
|
24
|
-
supportsStreaming: true,
|
|
25
|
-
supportsImages: false,
|
|
26
|
-
supportsTemperature: true,
|
|
27
|
-
supportsWebSearch: false,
|
|
28
|
-
supportsThinking: true,
|
|
29
|
-
timeout: 300000,
|
|
30
|
-
description:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
},
|
|
59
|
-
'
|
|
60
|
-
modelName: '
|
|
61
|
-
friendlyName: '
|
|
62
|
-
contextWindow:
|
|
63
|
-
maxOutputTokens: 8192,
|
|
64
|
-
supportsStreaming: true,
|
|
65
|
-
supportsImages: false,
|
|
66
|
-
supportsTemperature: true,
|
|
67
|
-
supportsWebSearch: false,
|
|
68
|
-
timeout: 300000,
|
|
69
|
-
description: '
|
|
70
|
-
aliases: [
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
openrouterProvider.
|
|
288
|
-
// First check static models
|
|
289
|
-
const staticConfig =
|
|
290
|
-
if (staticConfig) {
|
|
291
|
-
return staticConfig;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Check dynamic models cache
|
|
295
|
-
if (dynamicModels.has(modelName)) {
|
|
296
|
-
return dynamicModels.get(modelName);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
//
|
|
300
|
-
const config = this._lastConfig || {};
|
|
301
|
-
const dynamicModelsEnabled =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
//
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (modelName) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter Provider
|
|
3
|
+
*
|
|
4
|
+
* Provider implementation for OpenRouter's unified API gateway using OpenAI-compatible API.
|
|
5
|
+
* Implements the unified interface: async invoke(messages, options) => { content, stop_reason, rawResponse }
|
|
6
|
+
*
|
|
7
|
+
* OpenRouter provides access to multiple AI models through a single API endpoint.
|
|
8
|
+
* IMPORTANT: Requires HTTP-Referer header for compliance tracking.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createOpenAICompatibleProvider } from './openai-compatible.js';
|
|
12
|
+
import { debugLog } from '../utils/console.js';
|
|
13
|
+
import { ProviderError, ErrorCodes } from './interface.js';
|
|
14
|
+
import { fetchModelEndpointsWithCache } from './openrouter-endpoints-client.js';
|
|
15
|
+
|
|
16
|
+
// Define supported OpenRouter models with their capabilities
|
|
17
|
+
// Only including the three specific models requested
|
|
18
|
+
const SUPPORTED_MODELS = {
|
|
19
|
+
'qwen/qwen3-235b-a22b-thinking-2507': {
|
|
20
|
+
modelName: 'qwen/qwen3-235b-a22b-thinking-2507',
|
|
21
|
+
friendlyName: 'Qwen3 235B Thinking (via OpenRouter)',
|
|
22
|
+
contextWindow: 32768,
|
|
23
|
+
maxOutputTokens: 8192,
|
|
24
|
+
supportsStreaming: true,
|
|
25
|
+
supportsImages: false,
|
|
26
|
+
supportsTemperature: true,
|
|
27
|
+
supportsWebSearch: false,
|
|
28
|
+
supportsThinking: true,
|
|
29
|
+
timeout: 300000,
|
|
30
|
+
description:
|
|
31
|
+
'Qwen3 235B Thinking model with enhanced reasoning capabilities',
|
|
32
|
+
aliases: [
|
|
33
|
+
'qwen3-thinking',
|
|
34
|
+
'qwen-thinking',
|
|
35
|
+
'qwen3 thinking',
|
|
36
|
+
'qwen thinking',
|
|
37
|
+
'qwen3-235b-thinking',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
'qwen/qwen3-coder': {
|
|
41
|
+
modelName: 'qwen/qwen3-coder',
|
|
42
|
+
friendlyName: 'Qwen3 Coder (via OpenRouter)',
|
|
43
|
+
contextWindow: 32768,
|
|
44
|
+
maxOutputTokens: 8192,
|
|
45
|
+
supportsStreaming: true,
|
|
46
|
+
supportsImages: false,
|
|
47
|
+
supportsTemperature: true,
|
|
48
|
+
supportsWebSearch: false,
|
|
49
|
+
timeout: 300000,
|
|
50
|
+
description: 'Qwen3 Coder specialized for programming tasks',
|
|
51
|
+
aliases: [
|
|
52
|
+
'qwen3-coder',
|
|
53
|
+
'qwen-coder',
|
|
54
|
+
'qwen3 coder',
|
|
55
|
+
'qwen coder',
|
|
56
|
+
'qwen-3-coder',
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
'moonshotai/kimi-k2': {
|
|
60
|
+
modelName: 'moonshotai/kimi-k2',
|
|
61
|
+
friendlyName: 'Kimi K2 (via OpenRouter)',
|
|
62
|
+
contextWindow: 200000,
|
|
63
|
+
maxOutputTokens: 8192,
|
|
64
|
+
supportsStreaming: true,
|
|
65
|
+
supportsImages: false,
|
|
66
|
+
supportsTemperature: true,
|
|
67
|
+
supportsWebSearch: false,
|
|
68
|
+
timeout: 300000,
|
|
69
|
+
description: 'Moonshot AI Kimi K2 with extended context window',
|
|
70
|
+
aliases: [
|
|
71
|
+
'kimi-k2',
|
|
72
|
+
'moonshot-kimi',
|
|
73
|
+
'kimi k2',
|
|
74
|
+
'kimi',
|
|
75
|
+
'moonshot kimi',
|
|
76
|
+
'moonshot-k2',
|
|
77
|
+
'k2',
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
'openrouter/auto': {
|
|
81
|
+
modelName: 'openrouter/auto',
|
|
82
|
+
friendlyName: 'OpenRouter Auto (via NotDiamond)',
|
|
83
|
+
contextWindow: 128000, // Safe default for auto-routing
|
|
84
|
+
maxOutputTokens: 8192, // Safe default
|
|
85
|
+
supportsStreaming: true,
|
|
86
|
+
supportsImages: false, // Conservative default
|
|
87
|
+
supportsTemperature: true,
|
|
88
|
+
supportsWebSearch: false,
|
|
89
|
+
timeout: 300000,
|
|
90
|
+
description:
|
|
91
|
+
'Auto-selects the best model for your prompt using NotDiamond routing',
|
|
92
|
+
aliases: [
|
|
93
|
+
'openrouter auto',
|
|
94
|
+
'auto router',
|
|
95
|
+
'auto-router',
|
|
96
|
+
'openrouter-auto',
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
'z-ai/glm-4.6': {
|
|
100
|
+
modelName: 'z-ai/glm-4.6',
|
|
101
|
+
friendlyName: 'Z.AI GLM 4.6 (via OpenRouter)',
|
|
102
|
+
contextWindow: 202752,
|
|
103
|
+
maxOutputTokens: 8192,
|
|
104
|
+
supportsStreaming: true,
|
|
105
|
+
supportsImages: false,
|
|
106
|
+
supportsTemperature: true,
|
|
107
|
+
supportsWebSearch: false,
|
|
108
|
+
timeout: 300000,
|
|
109
|
+
description:
|
|
110
|
+
'Z.AI GLM 4.6 with 200K context - improved coding, reasoning, and agent performance',
|
|
111
|
+
aliases: [
|
|
112
|
+
'glm-4.6',
|
|
113
|
+
'glm4.6',
|
|
114
|
+
'glm 4.6',
|
|
115
|
+
'z-ai glm',
|
|
116
|
+
'z-ai-glm',
|
|
117
|
+
'zai-glm',
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// OpenRouter error class
|
|
123
|
+
class OpenRouterProviderError extends ProviderError {
|
|
124
|
+
constructor(message, code, originalError = null) {
|
|
125
|
+
super(message, code, originalError);
|
|
126
|
+
this.name = 'OpenRouterProviderError';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate OpenRouter API key format
|
|
132
|
+
*/
|
|
133
|
+
function validateApiKey(apiKey) {
|
|
134
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// OpenRouter API keys typically start with 'sk-or-' and are 40+ characters
|
|
139
|
+
return apiKey.startsWith('sk-or-') && apiKey.length >= 40;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get custom headers for OpenRouter
|
|
144
|
+
*/
|
|
145
|
+
function getCustomHeaders(config) {
|
|
146
|
+
const headers = {};
|
|
147
|
+
|
|
148
|
+
// REQUIRED: HTTP-Referer header for compliance
|
|
149
|
+
// Handle both camelCase (from tests) and lowercase (from config.js) keys
|
|
150
|
+
const referer =
|
|
151
|
+
config?.providers?.openrouterreferer ||
|
|
152
|
+
config?.providers?.openrouterReferer ||
|
|
153
|
+
'https://github.com/FallDownTheSystem/converse';
|
|
154
|
+
headers['HTTP-Referer'] = referer;
|
|
155
|
+
|
|
156
|
+
// Optional: X-Title header for request tracking
|
|
157
|
+
const title =
|
|
158
|
+
config?.providers?.openroutertitle || config?.providers?.openrouterTitle;
|
|
159
|
+
if (title) {
|
|
160
|
+
headers['X-Title'] = title;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
debugLog(`[OpenRouter] Using referer: ${referer}`);
|
|
164
|
+
|
|
165
|
+
return headers;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Transform request to handle OpenRouter-specific requirements
|
|
170
|
+
*/
|
|
171
|
+
async function transformRequest(requestPayload, { modelConfig }) {
|
|
172
|
+
// OpenRouter supports additional parameters
|
|
173
|
+
const transformed = { ...requestPayload };
|
|
174
|
+
|
|
175
|
+
// Ensure model name includes provider prefix if not already present
|
|
176
|
+
if (!transformed.model.includes('/')) {
|
|
177
|
+
debugLog(
|
|
178
|
+
`[OpenRouter] Warning: Model name '${transformed.model}' should include provider prefix (e.g., 'anthropic/claude-3.5-sonnet')`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// OpenRouter supports provider-specific parameters through 'provider' field
|
|
183
|
+
// This is useful for passing model-specific settings
|
|
184
|
+
if (modelConfig.providerSettings) {
|
|
185
|
+
transformed.provider = modelConfig.providerSettings;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return transformed;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Transform response to handle OpenRouter-specific fields
|
|
193
|
+
*/
|
|
194
|
+
async function transformResponse(result, rawResponse) {
|
|
195
|
+
// OpenRouter adds additional metadata
|
|
196
|
+
if (rawResponse.id) {
|
|
197
|
+
result.metadata.request_id = rawResponse.id;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// OpenRouter provides pricing information
|
|
201
|
+
if (rawResponse.usage) {
|
|
202
|
+
if (rawResponse.usage.prompt_cost) {
|
|
203
|
+
result.metadata.prompt_cost = rawResponse.usage.prompt_cost;
|
|
204
|
+
}
|
|
205
|
+
if (rawResponse.usage.completion_cost) {
|
|
206
|
+
result.metadata.completion_cost = rawResponse.usage.completion_cost;
|
|
207
|
+
}
|
|
208
|
+
if (rawResponse.usage.total_cost) {
|
|
209
|
+
result.metadata.total_cost = rawResponse.usage.total_cost;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// OpenRouter may return the actual provider used
|
|
214
|
+
if (rawResponse.provider) {
|
|
215
|
+
result.metadata.actual_provider = rawResponse.provider;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create OpenRouter provider using OpenAI-compatible base
|
|
223
|
+
*/
|
|
224
|
+
export const openrouterProvider = createOpenAICompatibleProvider({
|
|
225
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
226
|
+
providerName: 'OpenRouter',
|
|
227
|
+
supportedModels: SUPPORTED_MODELS,
|
|
228
|
+
validateApiKey,
|
|
229
|
+
transformRequest,
|
|
230
|
+
transformResponse,
|
|
231
|
+
customHeaders: {}, // Headers are dynamic, set via getCustomHeaders
|
|
232
|
+
defaultParams: {
|
|
233
|
+
// OpenRouter default parameters
|
|
234
|
+
top_p: 1,
|
|
235
|
+
frequency_penalty: 0,
|
|
236
|
+
presence_penalty: 0,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if a model string follows OpenRouter's provider/model format
|
|
242
|
+
*/
|
|
243
|
+
function isOpenRouterModelFormat(modelName) {
|
|
244
|
+
return typeof modelName === 'string' && modelName.includes('/');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Create a dynamic model configuration from minimal information
|
|
249
|
+
*/
|
|
250
|
+
function createDynamicModelConfig(modelName) {
|
|
251
|
+
return {
|
|
252
|
+
modelName,
|
|
253
|
+
friendlyName: `${modelName} (via OpenRouter)`,
|
|
254
|
+
contextWindow: 8192, // Safe default
|
|
255
|
+
maxOutputTokens: 4096, // Safe default
|
|
256
|
+
supportsStreaming: true,
|
|
257
|
+
supportsImages: false, // Conservative default
|
|
258
|
+
supportsTemperature: true,
|
|
259
|
+
supportsWebSearch: false,
|
|
260
|
+
timeout: 300000,
|
|
261
|
+
description: `Dynamic model: ${modelName}`,
|
|
262
|
+
isDynamic: true, // Flag to identify dynamic models
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Store for dynamically discovered models
|
|
267
|
+
const dynamicModels = new Map();
|
|
268
|
+
|
|
269
|
+
// Override methods to support dynamic models
|
|
270
|
+
const originalGetSupportedModels = openrouterProvider.getSupportedModels;
|
|
271
|
+
openrouterProvider.getSupportedModels = function () {
|
|
272
|
+
const staticModels = originalGetSupportedModels.call(this);
|
|
273
|
+
|
|
274
|
+
// Merge dynamic models if any exist
|
|
275
|
+
if (dynamicModels.size > 0) {
|
|
276
|
+
const allModels = { ...staticModels };
|
|
277
|
+
for (const [modelName, config] of dynamicModels) {
|
|
278
|
+
allModels[modelName] = config;
|
|
279
|
+
}
|
|
280
|
+
return allModels;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return staticModels;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Create an async version of getModelConfig for API fetching
|
|
287
|
+
openrouterProvider.getModelConfigAsync = async function (modelName) {
|
|
288
|
+
// First check static models
|
|
289
|
+
const staticConfig = this.getModelConfig(modelName);
|
|
290
|
+
if (staticConfig && !staticConfig.isDynamic) {
|
|
291
|
+
return staticConfig;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check if already in dynamic models cache
|
|
295
|
+
if (dynamicModels.has(modelName)) {
|
|
296
|
+
return dynamicModels.get(modelName);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// If dynamic models are enabled and model follows format, fetch from API
|
|
300
|
+
const config = this._lastConfig || {};
|
|
301
|
+
const dynamicModelsEnabled =
|
|
302
|
+
config?.providers?.openrouterdynamicmodels ||
|
|
303
|
+
config?.providers?.openrouterDynamicModels;
|
|
304
|
+
if (dynamicModelsEnabled && isOpenRouterModelFormat(modelName)) {
|
|
305
|
+
debugLog(`[OpenRouter] Fetching dynamic model config for: ${modelName}`);
|
|
306
|
+
|
|
307
|
+
// Fetch from API with caching
|
|
308
|
+
const apiConfig = await fetchModelEndpointsWithCache(modelName);
|
|
309
|
+
|
|
310
|
+
if (apiConfig) {
|
|
311
|
+
// Store in dynamic models cache
|
|
312
|
+
dynamicModels.set(modelName, apiConfig);
|
|
313
|
+
return apiConfig;
|
|
314
|
+
} else {
|
|
315
|
+
// Model not found on API, create default config to avoid repeated lookups
|
|
316
|
+
const defaultConfig = createDynamicModelConfig(modelName);
|
|
317
|
+
defaultConfig.notFoundOnApi = true;
|
|
318
|
+
dynamicModels.set(modelName, defaultConfig);
|
|
319
|
+
return defaultConfig;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return null;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const originalGetModelConfig = openrouterProvider.getModelConfig;
|
|
327
|
+
openrouterProvider.getModelConfig = function (modelName) {
|
|
328
|
+
// First check static models
|
|
329
|
+
const staticConfig = originalGetModelConfig.call(this, modelName);
|
|
330
|
+
if (staticConfig) {
|
|
331
|
+
return staticConfig;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check dynamic models cache
|
|
335
|
+
if (dynamicModels.has(modelName)) {
|
|
336
|
+
return dynamicModels.get(modelName);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check if dynamic models are enabled
|
|
340
|
+
const config = this._lastConfig || {};
|
|
341
|
+
const dynamicModelsEnabled =
|
|
342
|
+
config?.providers?.openrouterdynamicmodels ||
|
|
343
|
+
config?.providers?.openrouterDynamicModels;
|
|
344
|
+
|
|
345
|
+
// Only allow dynamic models if explicitly enabled AND model has slash format
|
|
346
|
+
if (dynamicModelsEnabled && isOpenRouterModelFormat(modelName)) {
|
|
347
|
+
// Note: This is a fallback for synchronous calls
|
|
348
|
+
// The async version should be preferred for accurate model info
|
|
349
|
+
const dynamicConfig = createDynamicModelConfig(modelName);
|
|
350
|
+
dynamicConfig.needsApiUpdate = true;
|
|
351
|
+
return dynamicConfig;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// If model has slash format but dynamic models disabled, return null
|
|
355
|
+
// This will cause the model to be rejected as not found
|
|
356
|
+
return null;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Override the invoke method to add dynamic headers and model support
|
|
360
|
+
const originalInvoke = openrouterProvider.invoke;
|
|
361
|
+
openrouterProvider.invoke = async function (messages, options = {}) {
|
|
362
|
+
// Store config for use in getModelConfig
|
|
363
|
+
this._lastConfig = options.config;
|
|
364
|
+
|
|
365
|
+
// Validate referer configuration
|
|
366
|
+
// Handle both camelCase (from tests) and lowercase (from config.js) keys
|
|
367
|
+
if (
|
|
368
|
+
!options.config?.providers?.openrouterreferer &&
|
|
369
|
+
!options.config?.providers?.openrouterReferer
|
|
370
|
+
) {
|
|
371
|
+
throw new OpenRouterProviderError(
|
|
372
|
+
'OpenRouter requires HTTP-Referer header. Please set OPENROUTER_REFERER in your environment',
|
|
373
|
+
ErrorCodes.INVALID_REQUEST,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Check if we need to fetch dynamic model config
|
|
378
|
+
const modelName = options.model;
|
|
379
|
+
if (modelName) {
|
|
380
|
+
const existingConfig = this.getModelConfig(modelName);
|
|
381
|
+
|
|
382
|
+
// If model not found and has slash format, check if dynamic models are enabled
|
|
383
|
+
if (!existingConfig && isOpenRouterModelFormat(modelName)) {
|
|
384
|
+
const dynamicModelsEnabled =
|
|
385
|
+
options.config?.providers?.openrouterdynamicmodels ||
|
|
386
|
+
options.config?.providers?.openrouterDynamicModels;
|
|
387
|
+
if (!dynamicModelsEnabled) {
|
|
388
|
+
throw new OpenRouterProviderError(
|
|
389
|
+
`Model '${modelName}' requires OPENROUTER_DYNAMIC_MODELS=true to be set`,
|
|
390
|
+
ErrorCodes.MODEL_NOT_FOUND,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// If the model needs API update, fetch it now
|
|
396
|
+
if (existingConfig?.needsApiUpdate) {
|
|
397
|
+
const dynamicModelsEnabled =
|
|
398
|
+
options.config?.providers?.openrouterdynamicmodels ||
|
|
399
|
+
options.config?.providers?.openrouterDynamicModels;
|
|
400
|
+
if (dynamicModelsEnabled) {
|
|
401
|
+
debugLog(`[OpenRouter] Fetching API config for model: ${modelName}`);
|
|
402
|
+
await this.getModelConfigAsync(modelName);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Create a modified config with custom headers
|
|
408
|
+
const modifiedOptions = {
|
|
409
|
+
...options,
|
|
410
|
+
config: {
|
|
411
|
+
...options.config,
|
|
412
|
+
// Inject custom headers into the provider config
|
|
413
|
+
providers: {
|
|
414
|
+
...options.config.providers,
|
|
415
|
+
_customHeaders: getCustomHeaders(options.config),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Call original invoke with modified options
|
|
421
|
+
return originalInvoke.call(this, messages, modifiedOptions);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// Note: The base module needs to be updated to use _customHeaders if present
|
|
425
|
+
// This is a temporary workaround - in production, the openai-compatible.js
|
|
426
|
+
// should be updated to accept a function for customHeaders
|