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