modelmix 3.8.2 → 3.8.6

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/index.js CHANGED
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const generateJsonSchema = require('./schema');
8
8
  const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
9
9
  const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
10
+ const { MCPToolsManager } = require('./mcp-tools');
10
11
 
11
12
  class ModelMix {
12
13
 
@@ -16,6 +17,7 @@ class ModelMix {
16
17
  this.tools = {};
17
18
  this.toolClient = {};
18
19
  this.mcp = {};
20
+ this.mcpToolsManager = new MCPToolsManager();
19
21
  this.options = {
20
22
  max_tokens: 5000,
21
23
  temperature: 1, // 1 --> More creative, 0 --> More deterministic.
@@ -41,6 +43,7 @@ class ModelMix {
41
43
 
42
44
  }
43
45
 
46
+
44
47
  replace(keyValues) {
45
48
  this.config.replace = { ...this.config.replace, ...keyValues };
46
49
  return this;
@@ -68,7 +71,6 @@ class ModelMix {
68
71
  return this;
69
72
  }
70
73
 
71
- // --- Model addition methods ---
72
74
  gpt41({ options = {}, config = {} } = {}) {
73
75
  return this.attach('gpt-4.1', new MixOpenAI({ options, config }));
74
76
  }
@@ -90,13 +92,22 @@ class ModelMix {
90
92
  gpt45({ options = {}, config = {} } = {}) {
91
93
  return this.attach('gpt-4.5-preview', new MixOpenAI({ options, config }));
92
94
  }
93
- gptOss({ options = {}, config = {}, mix = { together: false, cerebras: false, groq: true, lmstudio: false } } = {}) {
95
+ gpt5({ options = {}, config = {} } = {}) {
96
+ return this.attach('gpt-5', new MixOpenAI({ options, config }));
97
+ }
98
+ gpt5mini({ options = {}, config = {} } = {}) {
99
+ return this.attach('gpt-5-mini', new MixOpenAI({ options, config }));
100
+ }
101
+ gpt5nano({ options = {}, config = {} } = {}) {
102
+ return this.attach('gpt-5-nano', new MixOpenAI({ options, config }));
103
+ }
104
+ gptOss({ options = {}, config = {}, mix = { together: false, cerebras: false, groq: true } } = {}) {
94
105
  if (mix.together) return this.attach('openai/gpt-oss-120b', new MixTogether({ options, config }));
95
106
  if (mix.cerebras) return this.attach('gpt-oss-120b', new MixCerebras({ options, config }));
96
107
  if (mix.groq) return this.attach('openai/gpt-oss-120b', new MixGroq({ options, config }));
97
- if (mix.lmstudio) return this.attach('openai/gpt-oss-120b', new MixLMStudio({ options, config }));
98
108
  return this;
99
109
  }
110
+
100
111
  opus4think({ options = {}, config = {} } = {}) {
101
112
  options = { ...MixAnthropic.thinkingOptions, ...options };
102
113
  return this.attach('claude-opus-4-20250514', new MixAnthropic({ options, config }));
@@ -132,7 +143,7 @@ class ModelMix {
132
143
  return this.attach('claude-3-5-haiku-20241022', new MixAnthropic({ options, config }));
133
144
  }
134
145
  gemini25flash({ options = {}, config = {} } = {}) {
135
- return this.attach('gemini-2.5-flash-preview-04-17', new MixGoogle({ options, config }));
146
+ return this.attach('gemini-2.5-flash', new MixGoogle({ options, config }));
136
147
  }
137
148
  gemini25proExp({ options = {}, config = {} } = {}) {
138
149
  return this.attach('gemini-2.5-pro-exp-03-25', new MixGoogle({ options, config }));
@@ -189,11 +200,15 @@ class ModelMix {
189
200
  }
190
201
 
191
202
  kimiK2({ options = {}, config = {}, mix = { together: false, groq: true } } = {}) {
192
- if (mix.together) this.attach('moonshotai/Kimi-K2-Instruct', new MixTogether({ options, config }));
193
- if (mix.groq) this.attach('moonshotai/kimi-k2-instruct', new MixGroq({ options, config }));
203
+ if (mix.together) this.attach('moonshotai/Kimi-K2-Instruct-0905', new MixTogether({ options, config }));
204
+ if (mix.groq) this.attach('moonshotai/kimi-k2-instruct-0905', new MixGroq({ options, config }));
194
205
  return this;
195
206
  }
196
207
 
208
+ lmstudio({ options = {}, config = {} } = {}) {
209
+ return this.attach('lmstudio', new MixLMStudio({ options, config }));
210
+ }
211
+
197
212
  addText(text, { role = "user" } = {}) {
198
213
  const content = [{
199
214
  type: "text",
@@ -236,6 +251,12 @@ class ModelMix {
236
251
  }
237
252
 
238
253
  addImage(filePath, { role = "user" } = {}) {
254
+ const absolutePath = path.resolve(filePath);
255
+
256
+ if (!fs.existsSync(absolutePath)) {
257
+ throw new Error(`Image file not found: ${filePath}`);
258
+ }
259
+
239
260
  this.messages.push({
240
261
  role,
241
262
  content: [{
@@ -250,24 +271,41 @@ class ModelMix {
250
271
  }
251
272
 
252
273
  addImageFromUrl(url, { role = "user" } = {}) {
274
+ let source;
275
+ if (url.startsWith('data:')) {
276
+ // Parse data URL: data:image/jpeg;base64,/9j/4AAQ...
277
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
278
+ if (match) {
279
+ source = {
280
+ type: "base64",
281
+ media_type: match[1],
282
+ data: match[2]
283
+ };
284
+ } else {
285
+ throw new Error('Invalid data URL format');
286
+ }
287
+ } else {
288
+ source = {
289
+ type: "url",
290
+ data: url
291
+ };
292
+ }
293
+
253
294
  this.messages.push({
254
295
  role,
255
296
  content: [{
256
297
  type: "image",
257
- source: {
258
- type: "url",
259
- data: url
260
- }
298
+ source
261
299
  }]
262
300
  });
301
+
263
302
  return this;
264
303
  }
265
304
 
266
305
  async processImages() {
267
- // Process images that are in messages
268
306
  for (let i = 0; i < this.messages.length; i++) {
269
307
  const message = this.messages[i];
270
- if (!message.content) continue;
308
+ if (!Array.isArray(message.content)) continue;
271
309
 
272
310
  for (let j = 0; j < message.content.length; j++) {
273
311
  const content = message.content[j];
@@ -381,8 +419,13 @@ class ModelMix {
381
419
  }
382
420
 
383
421
  replaceKeyFromFile(key, filePath) {
384
- const content = this.readFile(filePath);
385
- this.replace({ [key]: this._template(content, this.config.replace) });
422
+ try {
423
+ const content = this.readFile(filePath);
424
+ this.replace({ [key]: this._template(content, this.config.replace) });
425
+ } catch (error) {
426
+ // Gracefully handle file read errors without throwing
427
+ log.warn(`replaceKeyFromFile: ${error.message}`);
428
+ }
386
429
  return this;
387
430
  }
388
431
 
@@ -430,7 +473,28 @@ class ModelMix {
430
473
  async prepareMessages() {
431
474
  await this.processImages();
432
475
  this.applyTemplate();
433
- this.messages = this.messages.slice(-this.config.max_history);
476
+
477
+ // Smart message slicing to preserve tool call sequences
478
+ if (this.config.max_history > 0) {
479
+ let sliceStart = Math.max(0, this.messages.length - this.config.max_history);
480
+
481
+ // If we're slicing and there's a tool message at the start,
482
+ // ensure we include the preceding assistant message with tool_calls
483
+ while (sliceStart > 0 &&
484
+ sliceStart < this.messages.length &&
485
+ this.messages[sliceStart].role === 'tool') {
486
+ sliceStart--;
487
+ // Also need to include the assistant message with tool_calls
488
+ if (sliceStart > 0 &&
489
+ this.messages[sliceStart].role === 'assistant' &&
490
+ this.messages[sliceStart].tool_calls) {
491
+ break;
492
+ }
493
+ }
494
+
495
+ this.messages = this.messages.slice(sliceStart);
496
+ }
497
+
434
498
  this.messages = this.groupByRoles(this.messages);
435
499
  this.options.messages = this.messages;
436
500
  }
@@ -452,7 +516,7 @@ class ModelMix {
452
516
 
453
517
  async execute({ config = {}, options = {} } = {}) {
454
518
  if (!this.models || this.models.length === 0) {
455
- throw new Error("No models specified. Use methods like .gpt41mini(), .sonnet4() first.");
519
+ throw new Error("No models specified. Use methods like .gpt5(), .sonnet4() first.");
456
520
  }
457
521
 
458
522
  return this.limiter.schedule(async () => {
@@ -514,7 +578,7 @@ class ModelMix {
514
578
  }
515
579
  }
516
580
 
517
- this.messages.push({ role: "assistant", content: result.toolCalls, tool_calls: result.toolCalls });
581
+ this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
518
582
 
519
583
  const content = await this.processToolCalls(result.toolCalls);
520
584
  this.messages.push({ role: 'tool', content });
@@ -555,18 +619,67 @@ class ModelMix {
555
619
  const result = []
556
620
 
557
621
  for (const toolCall of toolCalls) {
558
- const client = this.toolClient[toolCall.function.name];
622
+ // Handle different tool call formats more robustly
623
+ let toolName, toolArgs, toolId;
624
+
625
+ try {
626
+ if (toolCall.function) {
627
+ // Formato OpenAI/normalizado
628
+ toolName = toolCall.function.name;
629
+ toolArgs = typeof toolCall.function.arguments === 'string'
630
+ ? JSON.parse(toolCall.function.arguments)
631
+ : toolCall.function.arguments;
632
+ toolId = toolCall.id;
633
+ } else if (toolCall.name) {
634
+ // Formato directo (posible formato alternativo)
635
+ toolName = toolCall.name;
636
+ toolArgs = toolCall.input || toolCall.arguments || {};
637
+ toolId = toolCall.id;
638
+ } else {
639
+ console.error('Unknown tool call format:', JSON.stringify(toolCall, null, 2));
640
+ continue;
641
+ }
559
642
 
560
- const response = await client.callTool({
561
- name: toolCall.function.name,
562
- arguments: JSON.parse(toolCall.function.arguments)
563
- });
643
+ // Validar que tenemos los datos necesarios
644
+ if (!toolName) {
645
+ console.error('Tool call missing name:', JSON.stringify(toolCall, null, 2));
646
+ continue;
647
+ }
564
648
 
565
- result.push({
566
- name: toolCall.function.name,
567
- tool_call_id: toolCall.id,
568
- content: response.content.map(item => item.text).join("\n")
569
- });
649
+ // Verificar si es una herramienta local registrada
650
+ if (this.mcpToolsManager.hasTool(toolName)) {
651
+ const response = await this.mcpToolsManager.executeTool(toolName, toolArgs);
652
+ result.push({
653
+ name: toolName,
654
+ tool_call_id: toolId,
655
+ content: response.content.map(item => item.text).join("\n")
656
+ });
657
+ } else {
658
+ // Usar el cliente MCP externo
659
+ const client = this.toolClient[toolName];
660
+ if (!client) {
661
+ throw new Error(`No client found for tool: ${toolName}`);
662
+ }
663
+
664
+ const response = await client.callTool({
665
+ name: toolName,
666
+ arguments: toolArgs
667
+ });
668
+
669
+ result.push({
670
+ name: toolName,
671
+ tool_call_id: toolId,
672
+ content: response.content.map(item => item.text).join("\n")
673
+ });
674
+ }
675
+ } catch (error) {
676
+ console.error(`Error processing tool call ${toolName}:`, error);
677
+ result.push({
678
+ name: toolName || 'unknown',
679
+ tool_call_id: toolId || 'unknown',
680
+ content: `Error: ${error.message}`
681
+ });
682
+ }
570
683
  }
571
684
  return result;
572
685
  }
@@ -613,6 +726,56 @@ class ModelMix {
613
726
  }
614
727
 
615
728
  }
729
+
730
+ addTool(toolDefinition, callback) {
731
+
732
+ if (this.config.max_history < 3) {
733
+ log.warn(`MCP ${toolDefinition.name} requires at least 3 max_history. Setting to 3.`);
734
+ this.config.max_history = 3;
735
+ }
736
+
737
+ this.mcpToolsManager.registerTool(toolDefinition, callback);
738
+
739
+ // Agregar la herramienta al sistema de tools para que sea incluida en las requests
740
+ if (!this.tools.local) {
741
+ this.tools.local = [];
742
+ }
743
+ this.tools.local.push({
744
+ name: toolDefinition.name,
745
+ description: toolDefinition.description,
746
+ inputSchema: toolDefinition.inputSchema
747
+ });
748
+
749
+ return this;
750
+ }
751
+
752
+ addTools(toolsWithCallbacks) {
753
+ for (const { tool, callback } of toolsWithCallbacks) {
754
+ this.addTool(tool, callback);
755
+ }
756
+ return this;
757
+ }
758
+
759
+ removeTool(toolName) {
760
+ this.mcpToolsManager.removeTool(toolName);
761
+
762
+ // Also remove from the tools system
763
+ if (this.tools.local) {
764
+ this.tools.local = this.tools.local.filter(tool => tool.name !== toolName);
765
+ }
766
+
767
+ return this;
768
+ }
769
+
770
+ listTools() {
771
+ const localTools = this.mcpToolsManager.getToolsForMCP();
772
+ const mcpTools = Object.values(this.tools).flat();
773
+
774
+ return {
775
+ local: localTools,
776
+ mcp: mcpTools.filter(tool => !localTools.find(local => local.name === tool.name))
777
+ };
778
+ }
616
779
  }
617
780
 
618
781
  class MixCustom {
@@ -620,7 +783,7 @@ class MixCustom {
620
783
  this.config = this.getDefaultConfig(config);
621
784
  this.options = this.getDefaultOptions(options);
622
785
  this.headers = this.getDefaultHeaders(headers);
623
- this.streamCallback = null; // Definimos streamCallback aquí
786
+ this.streamCallback = null; // Define streamCallback here
624
787
  }
625
788
 
626
789
  getDefaultOptions(customOptions) {
@@ -812,6 +975,15 @@ class MixOpenAI extends MixCustom {
812
975
  delete options.max_tokens;
813
976
  delete options.temperature;
814
977
  }
978
+
979
+ // Use max_completion_tokens and remove temperature for GPT-5 models
980
+ if (options.model?.includes('gpt-5')) {
981
+ if (options.max_tokens) {
982
+ options.max_completion_tokens = options.max_tokens;
983
+ delete options.max_tokens;
984
+ }
985
+ delete options.temperature;
986
+ }
815
987
 
816
988
  return super.create({ config, options });
817
989
  }
@@ -831,26 +1003,33 @@ class MixOpenAI extends MixCustom {
831
1003
 
832
1004
  if (message.role === 'tool') {
833
1005
  for (const content of message.content) {
834
- results.push({ role: 'tool', ...content })
1006
+ results.push({
1007
+ role: 'tool',
1008
+ tool_call_id: content.tool_call_id,
1009
+ content: content.content
1010
+ })
835
1011
  }
836
1012
  continue;
837
1013
  }
838
1014
 
839
- if (Array.isArray(message.content))
840
- for (const content of message.content) {
841
- if (content.type === 'image') {
842
- const { type, media_type, data } = content.source;
843
- message.content = [{
1015
+ if (Array.isArray(message.content)) {
1016
+ message.content = message.content.filter(content => content !== null && content !== undefined).map(content => {
1017
+ if (content && content.type === 'image') {
1018
+ const { media_type, data } = content.source;
1019
+ return {
844
1020
  type: 'image_url',
845
1021
  image_url: {
846
- url: `data:${media_type};${type},${data}`
1022
+ url: `data:${media_type};base64,${data}`
847
1023
  }
848
- }];
1024
+ };
849
1025
  }
850
- }
1026
+ return content;
1027
+ });
1028
+ }
851
1029
 
852
1030
  results.push(message);
853
1031
  }
1032
+
854
1033
  return results;
855
1034
  }
856
1035
 
@@ -915,7 +1094,16 @@ class MixAnthropic extends MixCustom {
915
1094
  delete options.response_format;
916
1095
 
917
1096
  options.system = config.system;
918
- return super.create({ config, options });
1097
+
1098
+ try {
1099
+ return await super.create({ config, options });
1100
+ } catch (error) {
1101
+ // Log the error details for debugging
1102
+ if (error.response && error.response.data) {
1103
+ console.error('Anthropic API Error:', JSON.stringify(error.response.data, null, 2));
1104
+ }
1105
+ throw error;
1106
+ }
919
1107
  }
920
1108
 
921
1109
  convertMessages(messages, config) {
@@ -923,7 +1111,27 @@ class MixAnthropic extends MixCustom {
923
1111
  }
924
1112
 
925
1113
  static convertMessages(messages, config) {
926
- return messages.map(message => {
1114
+ // Filter out orphaned tool results for Anthropic
1115
+ const filteredMessages = [];
1116
+ for (let i = 0; i < messages.length; i++) {
1117
+ if (messages[i].role === 'tool') {
1118
+ // Check if there's a preceding assistant message with tool_calls
1119
+ let foundToolCall = false;
1120
+ for (let j = i - 1; j >= 0; j--) {
1121
+ if (messages[j].role === 'assistant' && messages[j].tool_calls) {
1122
+ foundToolCall = true;
1123
+ break;
1124
+ }
1125
+ }
1126
+ if (!foundToolCall) {
1127
+ // Skip orphaned tool results
1128
+ continue;
1129
+ }
1130
+ }
1131
+ filteredMessages.push(messages[i]);
1132
+ }
1133
+
1134
+ return filteredMessages.map(message => {
927
1135
  if (message.role === 'tool') {
928
1136
  return {
929
1137
  role: "user",
@@ -935,17 +1143,31 @@ class MixAnthropic extends MixCustom {
935
1143
  }
936
1144
  }
937
1145
 
938
- message.content = message.content.map(content => {
939
- if (content.type === 'function') {
940
- return {
941
- type: 'tool_use',
942
- id: content.id,
943
- name: content.function.name,
944
- input: JSON.parse(content.function.arguments)
1146
+ // Handle messages with tool_calls (assistant messages that call tools)
1147
+ if (message.tool_calls) {
1148
+ const content = message.tool_calls.map(call => ({
1149
+ type: 'tool_use',
1150
+ id: call.id,
1151
+ name: call.function.name,
1152
+ input: JSON.parse(call.function.arguments)
1153
+ }));
1154
+ return { role: 'assistant', content };
1155
+ }
1156
+
1157
+ // Handle content conversion for other messages
1158
+ if (message.content && Array.isArray(message.content)) {
1159
+ message.content = message.content.filter(content => content !== null && content !== undefined).map(content => {
1160
+ if (content && content.type === 'function') {
1161
+ return {
1162
+ type: 'tool_use',
1163
+ id: content.id,
1164
+ name: content.function.name,
1165
+ input: JSON.parse(content.function.arguments)
1166
+ }
945
1167
  }
946
- }
947
- return content;
948
- });
1168
+ return content;
1169
+ });
1170
+ }
949
1171
 
950
1172
  return message;
951
1173
  });
@@ -1016,7 +1238,6 @@ class MixAnthropic extends MixCustom {
1016
1238
  for (const tool in tools) {
1017
1239
  for (const item of tools[tool]) {
1018
1240
  options.tools.push({
1019
- type: 'custom',
1020
1241
  name: item.name,
1021
1242
  description: item.description,
1022
1243
  input_schema: item.inputSchema
@@ -1277,6 +1498,19 @@ class MixGoogle extends MixCustom {
1277
1498
  static convertMessages(messages, config) {
1278
1499
  return messages.map(message => {
1279
1500
 
1501
+ // Handle assistant messages with tool_calls (content is null)
1502
+ if (message.role === 'assistant' && message.tool_calls) {
1503
+ return {
1504
+ role: 'model',
1505
+ parts: message.tool_calls.map(toolCall => ({
1506
+ functionCall: {
1507
+ name: toolCall.function.name,
1508
+ args: JSON.parse(toolCall.function.arguments)
1509
+ }
1510
+ }))
1511
+ }
1512
+ }
1513
+
1280
1514
  if (!Array.isArray(message.content)) return message;
1281
1515
  const role = (message.role === 'assistant' || message.role === 'tool') ? 'model' : 'user'
1282
1516
 
package/mcp-tools.js ADDED
@@ -0,0 +1,96 @@
1
+ const log = require('lemonlog')('ModelMix:MCP-Tools');
2
+
3
+ class MCPToolsManager {
4
+ constructor() {
5
+ this.tools = new Map();
6
+ this.callbacks = new Map();
7
+ }
8
+
9
+ registerTool(toolDefinition, callback) {
10
+ const { name, description, inputSchema } = toolDefinition;
11
+
12
+ if (!name || !description || !inputSchema) {
13
+ throw new Error('Tool definition must include name, description, and inputSchema');
14
+ }
15
+
16
+ if (typeof callback !== 'function') {
17
+ throw new Error('Callback must be a function');
18
+ }
19
+
20
+ // Registrar la herramienta
21
+ this.tools.set(name, {
22
+ name,
23
+ description,
24
+ inputSchema
25
+ });
26
+
27
+ // Registrar el callback
28
+ this.callbacks.set(name, callback);
29
+
30
+ log.debug(`Tool registered: ${name}`);
31
+ }
32
+
33
+ registerTools(toolsWithCallbacks) {
34
+ for (const { tool, callback } of toolsWithCallbacks) {
35
+ this.registerTool(tool, callback);
36
+ }
37
+ }
38
+
39
+ async executeTool(name, args) {
40
+ const callback = this.callbacks.get(name);
41
+ if (!callback) {
42
+ throw new Error(`Tool not found: ${name}`);
43
+ }
44
+
45
+ try {
46
+ const result = await callback(args);
47
+ // For primitive values (numbers, booleans), convert to string
48
+ // For objects/arrays, stringify them
49
+ let textResult;
50
+ if (typeof result === 'string') {
51
+ textResult = result;
52
+ } else if (typeof result === 'number' || typeof result === 'boolean') {
53
+ textResult = String(result);
54
+ } else {
55
+ textResult = JSON.stringify(result, null, 2);
56
+ }
57
+
58
+ return {
59
+ content: [{
60
+ type: "text",
61
+ text: textResult
62
+ }]
63
+ };
64
+ } catch (error) {
65
+ log.error(`Error executing tool ${name}:`, error);
66
+ return {
67
+ content: [{
68
+ type: "text",
69
+ text: `Error executing ${name}: ${error.message}`
70
+ }]
71
+ };
72
+ }
73
+ }
74
+
75
+ getToolsForMCP() {
76
+ return Array.from(this.tools.values());
77
+ }
78
+
79
+ hasTool(name) {
80
+ return this.tools.has(name);
81
+ }
82
+
83
+ removeTool(name) {
84
+ this.tools.delete(name);
85
+ this.callbacks.delete(name);
86
+ log.debug(`Tool removed: ${name}`);
87
+ }
88
+
89
+ clear() {
90
+ this.tools.clear();
91
+ this.callbacks.clear();
92
+ log.debug('All tools cleared');
93
+ }
94
+ }
95
+
96
+ module.exports = { MCPToolsManager };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "3.8.2",
3
+ "version": "3.8.6",
4
4
  "description": "🧬 ModelMix - Unified API for Diverse AI LLM.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -15,13 +15,14 @@
15
15
  "openai",
16
16
  "anthropic",
17
17
  "agent",
18
- "perplexity",
19
18
  "grok4",
20
19
  "gpt",
21
20
  "claude",
22
21
  "llama",
22
+ "fallback",
23
23
  "kimi",
24
24
  "chat",
25
+ "gpt5",
25
26
  "opus",
26
27
  "sonnet",
27
28
  "multimodal",
@@ -29,18 +30,14 @@
29
30
  "gemini",
30
31
  "ollama",
31
32
  "lmstudio",
32
- "together",
33
33
  "nano",
34
34
  "deepseek",
35
35
  "oss",
36
- "4.1",
37
- "qwen",
38
36
  "nousresearch",
39
37
  "reasoning",
40
38
  "bottleneck",
41
39
  "cerebras",
42
40
  "scout",
43
- "fallback",
44
41
  "clasen"
45
42
  ],
46
43
  "author": "Martin Clasen",
@@ -55,6 +52,24 @@
55
52
  "bottleneck": "^2.19.5",
56
53
  "file-type": "^16.5.4",
57
54
  "form-data": "^4.0.4",
58
- "lemonlog": "^1.1.2"
55
+ "lemonlog": "^1.2.0"
56
+ },
57
+ "devDependencies": {
58
+ "chai": "^5.2.1",
59
+ "dotenv": "^17.2.1",
60
+ "mocha": "^11.7.1",
61
+ "nock": "^14.0.9",
62
+ "sinon": "^21.0.0"
63
+ },
64
+ "scripts": {
65
+ "test": "mocha test/**/*.js --timeout 10000 --require dotenv/config --require test/setup.js",
66
+ "test:watch": "mocha test/**/*.js --watch --timeout 10000 --require test/setup.js",
67
+ "test:json": "mocha test/json.test.js --timeout 10000 --require test/setup.js",
68
+ "test:fallback": "mocha test/fallback.test.js --timeout 10000 --require test/setup.js",
69
+ "test:templates": "mocha test/templates.test.js --timeout 10000 --require test/setup.js",
70
+ "test:images": "mocha test/images.test.js --timeout 10000 --require test/setup.js",
71
+ "test:bottleneck": "mocha test/bottleneck.test.js --timeout 10000 --require test/setup.js",
72
+ "test:live": "mocha test/live.test.js --timeout 10000 --require dotenv/config --require test/setup.js",
73
+ "test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require dotenv/config --require test/setup.js"
59
74
  }
60
75
  }