modelmix 3.8.6 → 3.9.0
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 +1 -3
- package/index.js +41 -53
- package/package.json +2 -2
- package/test/bottleneck.test.js +26 -0
- package/test/fallback.test.js +5 -0
- package/test/live.mcp.js +1 -1
- package/test/live.test.js +3 -3
- package/test/setup.js +25 -0
- package/test/templates.test.js +5 -0
package/README.md
CHANGED
|
@@ -126,9 +126,8 @@ Here's a comprehensive list of available methods:
|
|
|
126
126
|
| `o3()` | OpenAI | o3 | [\$10.00 / \$40.00][1] |
|
|
127
127
|
| `gptOss()` | Together | gpt-oss-120B | [\$0.15 / \$0.60][7] |
|
|
128
128
|
| `opus41[think]()` | Anthropic | claude-opus-4-1-20250805 | [\$15.00 / \$75.00][2] |
|
|
129
|
+
| `sonnet45[think]()`| Anthropic | claude-sonnet-4-5-20250929 | [\$3.00 / \$15.00][2] |
|
|
129
130
|
| `sonnet4[think]()` | Anthropic | claude-sonnet-4-20250514 | [\$3.00 / \$15.00][2] |
|
|
130
|
-
| `sonnet37[think]()`| Anthropic | claude-3-7-sonnet-20250219 | [\$3.00 / \$15.00][2] |
|
|
131
|
-
| `sonnet35()` | Anthropic | claude-3-5-sonnet-20241022 | [\$3.00 / \$15.00][2] |
|
|
132
131
|
| `haiku35()` | Anthropic | claude-3-5-haiku-20241022 | [\$0.80 / \$4.00][2] |
|
|
133
132
|
| `gemini25flash()` | Google | gemini-2.5-flash-preview-04-17 | [\$0.00 / \$0.00][3] |
|
|
134
133
|
| `gemini25proExp()` | Google | gemini-2.5-pro-exp-03-25 | [\$0.00 / \$0.00][3] |
|
|
@@ -331,7 +330,6 @@ new ModelMix(args = { options: {}, config: {} })
|
|
|
331
330
|
- **options**: This object contains default options that are applied to all models. These options can be overridden when creating a specific model instance. Examples of default options include:
|
|
332
331
|
- `max_tokens`: Sets the maximum number of tokens to generate, e.g., 2000.
|
|
333
332
|
- `temperature`: Controls the randomness of the model's output, e.g., 1.
|
|
334
|
-
- `top_p`: Controls the diversity of the output, e.g., 1.
|
|
335
333
|
- ...(Additional default options can be added as needed)
|
|
336
334
|
- **config**: This object contains configuration settings that control the behavior of the `ModelMix` instance. These settings can also be overridden for specific model instances. Examples of configuration settings include:
|
|
337
335
|
- `system`: Sets the default system message for the model, e.g., "You are an assistant."
|
package/index.js
CHANGED
|
@@ -21,7 +21,6 @@ class ModelMix {
|
|
|
21
21
|
this.options = {
|
|
22
22
|
max_tokens: 5000,
|
|
23
23
|
temperature: 1, // 1 --> More creative, 0 --> More deterministic.
|
|
24
|
-
top_p: 1, // 100% --> The model considers all possible tokens.
|
|
25
24
|
...options
|
|
26
25
|
};
|
|
27
26
|
|
|
@@ -100,21 +99,14 @@ class ModelMix {
|
|
|
100
99
|
}
|
|
101
100
|
gpt5nano({ options = {}, config = {} } = {}) {
|
|
102
101
|
return this.attach('gpt-5-nano', new MixOpenAI({ options, config }));
|
|
103
|
-
}
|
|
102
|
+
}
|
|
104
103
|
gptOss({ options = {}, config = {}, mix = { together: false, cerebras: false, groq: true } } = {}) {
|
|
105
104
|
if (mix.together) return this.attach('openai/gpt-oss-120b', new MixTogether({ options, config }));
|
|
106
105
|
if (mix.cerebras) return this.attach('gpt-oss-120b', new MixCerebras({ options, config }));
|
|
107
106
|
if (mix.groq) return this.attach('openai/gpt-oss-120b', new MixGroq({ options, config }));
|
|
108
107
|
return this;
|
|
109
108
|
}
|
|
110
|
-
|
|
111
|
-
opus4think({ options = {}, config = {} } = {}) {
|
|
112
|
-
options = { ...MixAnthropic.thinkingOptions, ...options };
|
|
113
|
-
return this.attach('claude-opus-4-20250514', new MixAnthropic({ options, config }));
|
|
114
|
-
}
|
|
115
|
-
opus4({ options = {}, config = {} } = {}) {
|
|
116
|
-
return this.attach('claude-opus-4-20250514', new MixAnthropic({ options, config }));
|
|
117
|
-
}
|
|
109
|
+
|
|
118
110
|
opus41({ options = {}, config = {} } = {}) {
|
|
119
111
|
return this.attach('claude-opus-4-1-20250805', new MixAnthropic({ options, config }));
|
|
120
112
|
}
|
|
@@ -129,6 +121,13 @@ class ModelMix {
|
|
|
129
121
|
options = { ...MixAnthropic.thinkingOptions, ...options };
|
|
130
122
|
return this.attach('claude-sonnet-4-20250514', new MixAnthropic({ options, config }));
|
|
131
123
|
}
|
|
124
|
+
sonnet45({ options = {}, config = {} } = {}) {
|
|
125
|
+
return this.attach('claude-sonnet-4-5-20250929', new MixAnthropic({ options, config }));
|
|
126
|
+
}
|
|
127
|
+
sonnet45think({ options = {}, config = {} } = {}) {
|
|
128
|
+
options = { ...MixAnthropic.thinkingOptions, ...options };
|
|
129
|
+
return this.attach('claude-sonnet-4-5-20250929', new MixAnthropic({ options, config }));
|
|
130
|
+
}
|
|
132
131
|
sonnet37({ options = {}, config = {} } = {}) {
|
|
133
132
|
return this.attach('claude-3-7-sonnet-20250219', new MixAnthropic({ options, config }));
|
|
134
133
|
}
|
|
@@ -136,9 +135,6 @@ class ModelMix {
|
|
|
136
135
|
options = { ...MixAnthropic.thinkingOptions, ...options };
|
|
137
136
|
return this.attach('claude-3-7-sonnet-20250219', new MixAnthropic({ options, config }));
|
|
138
137
|
}
|
|
139
|
-
sonnet35({ options = {}, config = {} } = {}) {
|
|
140
|
-
return this.attach('claude-3-5-sonnet-20241022', new MixAnthropic({ options, config }));
|
|
141
|
-
}
|
|
142
138
|
haiku35({ options = {}, config = {} } = {}) {
|
|
143
139
|
return this.attach('claude-3-5-haiku-20241022', new MixAnthropic({ options, config }));
|
|
144
140
|
}
|
|
@@ -252,7 +248,7 @@ class ModelMix {
|
|
|
252
248
|
|
|
253
249
|
addImage(filePath, { role = "user" } = {}) {
|
|
254
250
|
const absolutePath = path.resolve(filePath);
|
|
255
|
-
|
|
251
|
+
|
|
256
252
|
if (!fs.existsSync(absolutePath)) {
|
|
257
253
|
throw new Error(`Image file not found: ${filePath}`);
|
|
258
254
|
}
|
|
@@ -286,11 +282,11 @@ class ModelMix {
|
|
|
286
282
|
}
|
|
287
283
|
} else {
|
|
288
284
|
source = {
|
|
289
|
-
type: "url",
|
|
285
|
+
type: "url",
|
|
290
286
|
data: url
|
|
291
287
|
};
|
|
292
288
|
}
|
|
293
|
-
|
|
289
|
+
|
|
294
290
|
this.messages.push({
|
|
295
291
|
role,
|
|
296
292
|
content: [{
|
|
@@ -298,7 +294,7 @@ class ModelMix {
|
|
|
298
294
|
source
|
|
299
295
|
}]
|
|
300
296
|
});
|
|
301
|
-
|
|
297
|
+
|
|
302
298
|
return this;
|
|
303
299
|
}
|
|
304
300
|
|
|
@@ -473,28 +469,28 @@ class ModelMix {
|
|
|
473
469
|
async prepareMessages() {
|
|
474
470
|
await this.processImages();
|
|
475
471
|
this.applyTemplate();
|
|
476
|
-
|
|
472
|
+
|
|
477
473
|
// Smart message slicing to preserve tool call sequences
|
|
478
474
|
if (this.config.max_history > 0) {
|
|
479
475
|
let sliceStart = Math.max(0, this.messages.length - this.config.max_history);
|
|
480
|
-
|
|
476
|
+
|
|
481
477
|
// If we're slicing and there's a tool message at the start,
|
|
482
478
|
// ensure we include the preceding assistant message with tool_calls
|
|
483
|
-
while (sliceStart > 0 &&
|
|
484
|
-
|
|
485
|
-
|
|
479
|
+
while (sliceStart > 0 &&
|
|
480
|
+
sliceStart < this.messages.length &&
|
|
481
|
+
this.messages[sliceStart].role === 'tool') {
|
|
486
482
|
sliceStart--;
|
|
487
483
|
// Also need to include the assistant message with tool_calls
|
|
488
|
-
if (sliceStart > 0 &&
|
|
489
|
-
this.messages[sliceStart].role === 'assistant' &&
|
|
484
|
+
if (sliceStart > 0 &&
|
|
485
|
+
this.messages[sliceStart].role === 'assistant' &&
|
|
490
486
|
this.messages[sliceStart].tool_calls) {
|
|
491
487
|
break;
|
|
492
488
|
}
|
|
493
489
|
}
|
|
494
|
-
|
|
490
|
+
|
|
495
491
|
this.messages = this.messages.slice(sliceStart);
|
|
496
492
|
}
|
|
497
|
-
|
|
493
|
+
|
|
498
494
|
this.messages = this.groupByRoles(this.messages);
|
|
499
495
|
this.options.messages = this.messages;
|
|
500
496
|
}
|
|
@@ -621,13 +617,13 @@ class ModelMix {
|
|
|
621
617
|
for (const toolCall of toolCalls) {
|
|
622
618
|
// Handle different tool call formats more robustly
|
|
623
619
|
let toolName, toolArgs, toolId;
|
|
624
|
-
|
|
620
|
+
|
|
625
621
|
try {
|
|
626
622
|
if (toolCall.function) {
|
|
627
623
|
// Formato OpenAI/normalizado
|
|
628
624
|
toolName = toolCall.function.name;
|
|
629
|
-
toolArgs = typeof toolCall.function.arguments === 'string'
|
|
630
|
-
? JSON.parse(toolCall.function.arguments)
|
|
625
|
+
toolArgs = typeof toolCall.function.arguments === 'string'
|
|
626
|
+
? JSON.parse(toolCall.function.arguments)
|
|
631
627
|
: toolCall.function.arguments;
|
|
632
628
|
toolId = toolCall.id;
|
|
633
629
|
} else if (toolCall.name) {
|
|
@@ -735,7 +731,7 @@ class ModelMix {
|
|
|
735
731
|
}
|
|
736
732
|
|
|
737
733
|
this.mcpToolsManager.registerTool(toolDefinition, callback);
|
|
738
|
-
|
|
734
|
+
|
|
739
735
|
// Agregar la herramienta al sistema de tools para que sea incluida en las requests
|
|
740
736
|
if (!this.tools.local) {
|
|
741
737
|
this.tools.local = [];
|
|
@@ -745,7 +741,7 @@ class ModelMix {
|
|
|
745
741
|
description: toolDefinition.description,
|
|
746
742
|
inputSchema: toolDefinition.inputSchema
|
|
747
743
|
});
|
|
748
|
-
|
|
744
|
+
|
|
749
745
|
return this;
|
|
750
746
|
}
|
|
751
747
|
|
|
@@ -758,19 +754,19 @@ class ModelMix {
|
|
|
758
754
|
|
|
759
755
|
removeTool(toolName) {
|
|
760
756
|
this.mcpToolsManager.removeTool(toolName);
|
|
761
|
-
|
|
757
|
+
|
|
762
758
|
// Also remove from the tools system
|
|
763
759
|
if (this.tools.local) {
|
|
764
760
|
this.tools.local = this.tools.local.filter(tool => tool.name !== toolName);
|
|
765
761
|
}
|
|
766
|
-
|
|
762
|
+
|
|
767
763
|
return this;
|
|
768
764
|
}
|
|
769
765
|
|
|
770
766
|
listTools() {
|
|
771
767
|
const localTools = this.mcpToolsManager.getToolsForMCP();
|
|
772
768
|
const mcpTools = Object.values(this.tools).flat();
|
|
773
|
-
|
|
769
|
+
|
|
774
770
|
return {
|
|
775
771
|
local: localTools,
|
|
776
772
|
mcp: mcpTools.filter(tool => !localTools.find(local => local.name === tool.name))
|
|
@@ -975,7 +971,7 @@ class MixOpenAI extends MixCustom {
|
|
|
975
971
|
delete options.max_tokens;
|
|
976
972
|
delete options.temperature;
|
|
977
973
|
}
|
|
978
|
-
|
|
974
|
+
|
|
979
975
|
// Use max_completion_tokens and remove temperature for GPT-5 models
|
|
980
976
|
if (options.model?.includes('gpt-5')) {
|
|
981
977
|
if (options.max_tokens) {
|
|
@@ -1003,10 +999,10 @@ class MixOpenAI extends MixCustom {
|
|
|
1003
999
|
|
|
1004
1000
|
if (message.role === 'tool') {
|
|
1005
1001
|
for (const content of message.content) {
|
|
1006
|
-
results.push({
|
|
1007
|
-
role: 'tool',
|
|
1002
|
+
results.push({
|
|
1003
|
+
role: 'tool',
|
|
1008
1004
|
tool_call_id: content.tool_call_id,
|
|
1009
|
-
content: content.content
|
|
1005
|
+
content: content.content
|
|
1010
1006
|
})
|
|
1011
1007
|
}
|
|
1012
1008
|
continue;
|
|
@@ -1029,7 +1025,7 @@ class MixOpenAI extends MixCustom {
|
|
|
1029
1025
|
|
|
1030
1026
|
results.push(message);
|
|
1031
1027
|
}
|
|
1032
|
-
|
|
1028
|
+
|
|
1033
1029
|
return results;
|
|
1034
1030
|
}
|
|
1035
1031
|
|
|
@@ -1080,21 +1076,10 @@ class MixAnthropic extends MixCustom {
|
|
|
1080
1076
|
|
|
1081
1077
|
async create({ config = {}, options = {} } = {}) {
|
|
1082
1078
|
|
|
1083
|
-
// Remove top_p for thinking
|
|
1084
|
-
if (options.thinking) {
|
|
1085
|
-
delete options.top_p;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
if (options.model && options.model.includes('claude-opus-4-1')) {
|
|
1089
|
-
if (options.temperature !== undefined && options.top_p !== undefined) {
|
|
1090
|
-
delete options.top_p;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
1079
|
delete options.response_format;
|
|
1095
1080
|
|
|
1096
1081
|
options.system = config.system;
|
|
1097
|
-
|
|
1082
|
+
|
|
1098
1083
|
try {
|
|
1099
1084
|
return await super.create({ config, options });
|
|
1100
1085
|
} catch (error) {
|
|
@@ -1130,7 +1115,7 @@ class MixAnthropic extends MixCustom {
|
|
|
1130
1115
|
}
|
|
1131
1116
|
filteredMessages.push(messages[i]);
|
|
1132
1117
|
}
|
|
1133
|
-
|
|
1118
|
+
|
|
1134
1119
|
return filteredMessages.map(message => {
|
|
1135
1120
|
if (message.role === 'tool') {
|
|
1136
1121
|
return {
|
|
@@ -1575,10 +1560,13 @@ class MixGoogle extends MixCustom {
|
|
|
1575
1560
|
options.messages = MixGoogle.convertMessages(options.messages);
|
|
1576
1561
|
|
|
1577
1562
|
const generationConfig = {
|
|
1578
|
-
topP: options.top_p,
|
|
1579
1563
|
maxOutputTokens: options.max_tokens,
|
|
1580
1564
|
}
|
|
1581
1565
|
|
|
1566
|
+
if (options.top_p) {
|
|
1567
|
+
generationConfig.topP = options.top_p;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1582
1570
|
generationConfig.responseMimeType = "text/plain";
|
|
1583
1571
|
|
|
1584
1572
|
const payload = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "🧬 ModelMix - Unified API for Diverse AI LLM.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"homepage": "https://github.com/clasen/ModelMix#readme",
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.11.2",
|
|
51
|
-
"axios": "^1.
|
|
51
|
+
"axios": "^1.12.2",
|
|
52
52
|
"bottleneck": "^2.19.5",
|
|
53
53
|
"file-type": "^16.5.4",
|
|
54
54
|
"form-data": "^4.0.4",
|
package/test/bottleneck.test.js
CHANGED
|
@@ -6,6 +6,11 @@ const Bottleneck = require('bottleneck');
|
|
|
6
6
|
|
|
7
7
|
describe('Rate Limiting with Bottleneck Tests', () => {
|
|
8
8
|
|
|
9
|
+
// Setup test hooks
|
|
10
|
+
if (global.setupTestHooks) {
|
|
11
|
+
global.setupTestHooks();
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
afterEach(() => {
|
|
10
15
|
nock.cleanAll();
|
|
11
16
|
sinon.restore();
|
|
@@ -57,6 +62,13 @@ describe('Rate Limiting with Bottleneck Tests', () => {
|
|
|
57
62
|
});
|
|
58
63
|
});
|
|
59
64
|
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
// Clean up bottleneck state
|
|
67
|
+
if (model && model.limiter) {
|
|
68
|
+
await model.limiter.stop({ dropWaitingJobs: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
60
72
|
it('should enforce minimum time between requests', async () => {
|
|
61
73
|
const startTimes = [];
|
|
62
74
|
|
|
@@ -162,6 +174,13 @@ describe('Rate Limiting with Bottleneck Tests', () => {
|
|
|
162
174
|
});
|
|
163
175
|
});
|
|
164
176
|
|
|
177
|
+
afterEach(async () => {
|
|
178
|
+
// Clean up bottleneck state
|
|
179
|
+
if (model && model.limiter) {
|
|
180
|
+
await model.limiter.stop({ dropWaitingJobs: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
165
184
|
it('should apply rate limiting to OpenAI requests', async () => {
|
|
166
185
|
const requestTimes = [];
|
|
167
186
|
|
|
@@ -240,6 +259,13 @@ describe('Rate Limiting with Bottleneck Tests', () => {
|
|
|
240
259
|
});
|
|
241
260
|
});
|
|
242
261
|
|
|
262
|
+
afterEach(async () => {
|
|
263
|
+
// Clean up bottleneck state
|
|
264
|
+
if (model && model.limiter) {
|
|
265
|
+
await model.limiter.stop({ dropWaitingJobs: true });
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
243
269
|
it('should handle rate limiting with API errors', async () => {
|
|
244
270
|
model.gpt4o();
|
|
245
271
|
|
package/test/fallback.test.js
CHANGED
|
@@ -4,6 +4,11 @@ const nock = require('nock');
|
|
|
4
4
|
const { ModelMix } = require('../index.js');
|
|
5
5
|
|
|
6
6
|
describe('Provider Fallback Chain Tests', () => {
|
|
7
|
+
|
|
8
|
+
// Setup test hooks
|
|
9
|
+
if (global.setupTestHooks) {
|
|
10
|
+
global.setupTestHooks();
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
afterEach(() => {
|
|
9
14
|
nock.cleanAll();
|
package/test/live.mcp.js
CHANGED
|
@@ -528,7 +528,7 @@ describe('Live MCP Integration Tests', function () {
|
|
|
528
528
|
it('should work with same MCP tools across different Anthropic models', async function () {
|
|
529
529
|
const models = [
|
|
530
530
|
{ name: 'Sonnet 4', model: ModelMix.new(setup).sonnet4() },
|
|
531
|
-
{ name: 'Sonnet
|
|
531
|
+
{ name: 'Sonnet 4.5', model: ModelMix.new(setup).sonnet45() },
|
|
532
532
|
{ name: 'Haiku 3.5', model: ModelMix.new(setup).haiku35() }
|
|
533
533
|
];
|
|
534
534
|
|
package/test/live.test.js
CHANGED
|
@@ -31,7 +31,7 @@ describe('Live Integration Tests', function () {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it('should process images with Anthropic Claude', async function () {
|
|
34
|
-
const model = ModelMix.new(setup).
|
|
34
|
+
const model = ModelMix.new(setup).sonnet45();
|
|
35
35
|
|
|
36
36
|
model.addImageFromUrl(blueSquareBase64)
|
|
37
37
|
.addText('What color is this image? Answer in one word only.');
|
|
@@ -82,8 +82,8 @@ describe('Live Integration Tests', function () {
|
|
|
82
82
|
expect(result.skills).to.be.an('array');
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
it('should return structured JSON with Sonnet
|
|
86
|
-
const model = ModelMix.new(setup).
|
|
85
|
+
it('should return structured JSON with Sonnet 4.5 thinking', async function () {
|
|
86
|
+
const model = ModelMix.new(setup).sonnet45think();
|
|
87
87
|
|
|
88
88
|
model.addText('Generate information about a fictional city.');
|
|
89
89
|
|
package/test/setup.js
CHANGED
|
@@ -39,8 +39,24 @@ global.TEST_CONFIG = {
|
|
|
39
39
|
|
|
40
40
|
// Global cleanup function
|
|
41
41
|
global.cleanup = function() {
|
|
42
|
+
// More thorough nock cleanup
|
|
42
43
|
nock.cleanAll();
|
|
44
|
+
nock.restore();
|
|
45
|
+
nock.activate();
|
|
43
46
|
sinon.restore();
|
|
47
|
+
|
|
48
|
+
// Clear any pending timers and intervals
|
|
49
|
+
const highestTimeoutId = setTimeout(() => {}, 0);
|
|
50
|
+
clearTimeout(highestTimeoutId);
|
|
51
|
+
for (let i = 0; i < highestTimeoutId; i++) {
|
|
52
|
+
clearTimeout(i);
|
|
53
|
+
clearInterval(i);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Force garbage collection if available
|
|
57
|
+
if (global.gc) {
|
|
58
|
+
global.gc();
|
|
59
|
+
}
|
|
44
60
|
};
|
|
45
61
|
|
|
46
62
|
// Console override for testing (suppress debug logs unless needed)
|
|
@@ -151,6 +167,10 @@ global.testUtils = {
|
|
|
151
167
|
// Export hooks for tests to use
|
|
152
168
|
global.setupTestHooks = function() {
|
|
153
169
|
beforeEach(() => {
|
|
170
|
+
// Ensure clean state before each test
|
|
171
|
+
nock.cleanAll();
|
|
172
|
+
sinon.restore();
|
|
173
|
+
|
|
154
174
|
// Disable real HTTP requests
|
|
155
175
|
if (global.TEST_CONFIG.MOCK_APIS) {
|
|
156
176
|
nock.disableNetConnect();
|
|
@@ -165,6 +185,11 @@ global.setupTestHooks = function() {
|
|
|
165
185
|
if (global.TEST_CONFIG.MOCK_APIS) {
|
|
166
186
|
nock.enableNetConnect();
|
|
167
187
|
}
|
|
188
|
+
|
|
189
|
+
// Force garbage collection if available
|
|
190
|
+
if (global.gc) {
|
|
191
|
+
global.gc();
|
|
192
|
+
}
|
|
168
193
|
});
|
|
169
194
|
};
|
|
170
195
|
|
package/test/templates.test.js
CHANGED
|
@@ -6,6 +6,11 @@ const path = require('path');
|
|
|
6
6
|
const { ModelMix } = require('../index.js');
|
|
7
7
|
|
|
8
8
|
describe('Template and File Operations Tests', () => {
|
|
9
|
+
|
|
10
|
+
// Setup test hooks
|
|
11
|
+
if (global.setupTestHooks) {
|
|
12
|
+
global.setupTestHooks();
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
afterEach(() => {
|
|
11
16
|
nock.cleanAll();
|