modelmix 5.1.12 → 5.1.16
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 +30 -9
- package/demo/gemini.js +3 -3
- package/demo/short.js +1 -1
- package/effort.js +2 -1
- package/http-client.js +6 -6
- package/index.d.ts +11 -6
- package/index.js +136 -73
- package/lib/abort-signal.js +57 -0
- package/lib/model-chain.js +1 -1
- package/lib/providers/anthropic.js +2 -2
- package/lib/providers/base.js +17 -6
- package/lib/providers/google.js +9 -2
- package/lib/providers/openai-compatible.js +8 -8
- package/lib/providers/openai.js +32 -10
- package/lib/token-usage.js +1 -0
- package/mcp-tools.js +5 -2
- package/package.json +2 -2
- package/plugins/rlm/index.d.ts +1 -0
- package/plugins/rlm/lib/isolated-vm-sandbox.js +8 -1
- package/plugins/rlm/lib/plugin.js +23 -5
- package/plugins/rlm/test/isolated-vm-sandbox.test.js +23 -0
- package/skills/modelmix/SKILL.md +30 -12
- package/test/abort.test.js +517 -0
- package/test/effort.test.js +13 -0
- package/test/fallback.test.js +2 -2
- package/test/live.mcp.js +6 -6
- package/test/live.test.js +11 -6
- package/test/tokens.test.js +9 -6
package/lib/providers/google.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const { fetchJsonResponse } = require('../../http-client');
|
|
2
2
|
const { configForDebug } = require('../provider-debug');
|
|
3
3
|
const { requireProviderApiKey } = require('../provider-api-key');
|
|
4
|
+
const {
|
|
5
|
+
throwIfAborted,
|
|
6
|
+
validateProviderExecution
|
|
7
|
+
} = require('../abort-signal');
|
|
4
8
|
|
|
5
9
|
function createGoogleProviders({ ModelMix, MixCustom }) {
|
|
6
10
|
class MixGoogle extends MixCustom {
|
|
@@ -122,7 +126,8 @@ function createGoogleProviders({ ModelMix, MixCustom }) {
|
|
|
122
126
|
}, []);
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
async create({ config = {}, options = {} } = {}) {
|
|
129
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
130
|
+
validateProviderExecution(this, { config, options, signal });
|
|
126
131
|
const generateContentApi = options.stream ? 'streamGenerateContent' : 'generateContent';
|
|
127
132
|
|
|
128
133
|
const fullUrl = `${this.config.url}/${options.model}:${generateContentApi}?key=${this.config.apiKey}`;
|
|
@@ -187,10 +192,12 @@ function createGoogleProviders({ ModelMix, MixCustom }) {
|
|
|
187
192
|
return this.processResponse(await fetchJsonResponse(fullUrl, {
|
|
188
193
|
method: 'POST',
|
|
189
194
|
headers: this.headers,
|
|
190
|
-
body: JSON.stringify(payload)
|
|
195
|
+
body: JSON.stringify(payload),
|
|
196
|
+
signal
|
|
191
197
|
}));
|
|
192
198
|
}
|
|
193
199
|
} catch (error) {
|
|
200
|
+
throwIfAborted(signal);
|
|
194
201
|
throw this.handleError(error);
|
|
195
202
|
}
|
|
196
203
|
}
|
|
@@ -57,7 +57,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
async create({ config = {}, options = {} } = {}) {
|
|
60
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
61
61
|
|
|
62
62
|
if (config.schema) {
|
|
63
63
|
options.response_format = {
|
|
@@ -66,7 +66,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
return super.create({ config, options });
|
|
69
|
+
return super.create({ config, options, signal });
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -134,11 +134,11 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
async create({ config = {}, options = {} } = {}) {
|
|
137
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
138
138
|
if (options.model === GROK420_REASONING || options.model === GROK420_NON_REASONING) {
|
|
139
139
|
delete options.reasoning_effort;
|
|
140
140
|
}
|
|
141
|
-
return super.create({ config, options });
|
|
141
|
+
return super.create({ config, options, signal });
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -162,14 +162,14 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
create({ config = {}, options = {} } = {}) {
|
|
165
|
+
create({ config = {}, options = {}, signal } = {}) {
|
|
166
166
|
if (config.schema) {
|
|
167
167
|
options.response_format = {
|
|
168
168
|
type: 'json_schema',
|
|
169
169
|
json_schema: { schema: config.schema }
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
|
-
return super.create({ config, options });
|
|
172
|
+
return super.create({ config, options, signal });
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
static extractThink(data) {
|
|
@@ -261,9 +261,9 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
create({ config = {}, options = {} } = {}) {
|
|
264
|
+
create({ config = {}, options = {}, signal } = {}) {
|
|
265
265
|
delete options.response_format;
|
|
266
|
-
return super.create({ config, options });
|
|
266
|
+
return super.create({ config, options, signal });
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
package/lib/providers/openai.js
CHANGED
|
@@ -6,6 +6,10 @@ const {
|
|
|
6
6
|
} = require('../content-cache');
|
|
7
7
|
const { requireProviderApiKey } = require('../provider-api-key');
|
|
8
8
|
const { normalizeOpenAIOptions } = require('./openai-options');
|
|
9
|
+
const {
|
|
10
|
+
throwIfAborted,
|
|
11
|
+
validateProviderExecution
|
|
12
|
+
} = require('../abort-signal');
|
|
9
13
|
|
|
10
14
|
function createOpenAIProviders({
|
|
11
15
|
ModelMix,
|
|
@@ -17,15 +21,18 @@ function createOpenAIProviders({
|
|
|
17
21
|
const WebSocket = require('ws');
|
|
18
22
|
|
|
19
23
|
class MixOpenAIResponses extends MixOpenAI {
|
|
20
|
-
async create({ config = {}, options = {} } = {}) {
|
|
24
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
25
|
+
validateProviderExecution(this, { config, options, signal });
|
|
21
26
|
normalizeOpenAIOptions(options);
|
|
22
27
|
const responsesUrl = this.config.url.replace('/chat/completions', '/responses');
|
|
23
28
|
const request = MixOpenAIResponses.buildResponsesRequest(options, config);
|
|
24
29
|
const response = await fetchJsonResponse(responsesUrl, {
|
|
25
30
|
method: 'POST',
|
|
26
31
|
headers: this.headers,
|
|
27
|
-
body: JSON.stringify(request)
|
|
32
|
+
body: JSON.stringify(request),
|
|
33
|
+
signal
|
|
28
34
|
});
|
|
35
|
+
throwIfAborted(signal);
|
|
29
36
|
|
|
30
37
|
return MixOpenAIResponses.processResponsesResponse(response);
|
|
31
38
|
}
|
|
@@ -245,7 +252,8 @@ function createOpenAIProviders({
|
|
|
245
252
|
});
|
|
246
253
|
}
|
|
247
254
|
|
|
248
|
-
async create({ config = {}, options = {} } = {}) {
|
|
255
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
256
|
+
validateProviderExecution(this, { config, options, signal });
|
|
249
257
|
if (options.stream) {
|
|
250
258
|
throw new Error('Stream is not supported for OpenAI moderation');
|
|
251
259
|
}
|
|
@@ -254,8 +262,10 @@ function createOpenAIProviders({
|
|
|
254
262
|
const response = await fetchJsonResponse(this.config.url, {
|
|
255
263
|
method: 'POST',
|
|
256
264
|
headers: this.headers,
|
|
257
|
-
body: JSON.stringify({ model: options.model, input })
|
|
265
|
+
body: JSON.stringify({ model: options.model, input }),
|
|
266
|
+
signal
|
|
258
267
|
});
|
|
268
|
+
throwIfAborted(signal);
|
|
259
269
|
|
|
260
270
|
return {
|
|
261
271
|
moderation: response.data.results,
|
|
@@ -343,7 +353,8 @@ function createOpenAIProviders({
|
|
|
343
353
|
});
|
|
344
354
|
}
|
|
345
355
|
|
|
346
|
-
async create({ config = {}, options = {} } = {}) {
|
|
356
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
357
|
+
validateProviderExecution(this, { config, options, signal });
|
|
347
358
|
normalizeOpenAIOptions(options);
|
|
348
359
|
const mergedConfig = { ...this.config, ...config };
|
|
349
360
|
const realtimeUrl = `${mergedConfig.realtimeUrl}?model=${encodeURIComponent(options.model)}`;
|
|
@@ -372,7 +383,18 @@ function createOpenAIProviders({
|
|
|
372
383
|
});
|
|
373
384
|
}, timeoutMs);
|
|
374
385
|
|
|
375
|
-
const cleanUp = () =>
|
|
386
|
+
const cleanUp = () => {
|
|
387
|
+
clearTimeout(timeout);
|
|
388
|
+
signal?.removeEventListener('abort', onAbort);
|
|
389
|
+
};
|
|
390
|
+
const onAbort = () => {
|
|
391
|
+
if (settled) return;
|
|
392
|
+
settled = true;
|
|
393
|
+
cleanUp();
|
|
394
|
+
ws.terminate();
|
|
395
|
+
reject(signal.reason);
|
|
396
|
+
};
|
|
397
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
376
398
|
|
|
377
399
|
ws.on('open', () => {
|
|
378
400
|
const session = {
|
|
@@ -541,13 +563,13 @@ function createOpenAIProviders({
|
|
|
541
563
|
});
|
|
542
564
|
}
|
|
543
565
|
|
|
544
|
-
async create({ config = {}, options = {} } = {}) {
|
|
566
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
545
567
|
if (rejectsAnthropicSamplingParams(options.model)) {
|
|
546
568
|
delete options.temperature;
|
|
547
569
|
delete options.top_p;
|
|
548
570
|
delete options.top_k;
|
|
549
571
|
}
|
|
550
|
-
return super.create({ config, options });
|
|
572
|
+
return super.create({ config, options, signal });
|
|
551
573
|
}
|
|
552
574
|
}
|
|
553
575
|
|
|
@@ -562,7 +584,7 @@ function createOpenAIProviders({
|
|
|
562
584
|
});
|
|
563
585
|
}
|
|
564
586
|
|
|
565
|
-
async create({ config = {}, options = {} } = {}) {
|
|
587
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
566
588
|
if (Object.hasOwn(options, 'max_tokens')) {
|
|
567
589
|
options.max_completion_tokens = options.max_tokens;
|
|
568
590
|
delete options.max_tokens;
|
|
@@ -574,7 +596,7 @@ function createOpenAIProviders({
|
|
|
574
596
|
delete options.presence_penalty;
|
|
575
597
|
delete options.frequency_penalty;
|
|
576
598
|
|
|
577
|
-
return super.create({ config, options });
|
|
599
|
+
return super.create({ config, options, signal });
|
|
578
600
|
}
|
|
579
601
|
|
|
580
602
|
extractDelta(data) {
|
package/lib/token-usage.js
CHANGED
|
@@ -64,6 +64,7 @@ const MODEL_PRICING = {
|
|
|
64
64
|
'gemini-3.1-pro-preview': { input: 2.00, output: 12.00 },
|
|
65
65
|
'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
|
|
66
66
|
'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
|
|
67
|
+
'gemini-3.8-flash': { input: 0.75, cachedInput: 0.075, output: 3.75 },
|
|
67
68
|
'gemini-3.7-flash': { input: 0.75, cachedInput: 0.075, output: 3.75 },
|
|
68
69
|
'gemini-3.6-flash': { input: 0.75, cachedInput: 0.075, output: 3.75 },
|
|
69
70
|
'gemini-3.5-flash': { input: 0.75, output: 4.50 },
|
package/mcp-tools.js
CHANGED
|
@@ -36,14 +36,16 @@ class MCPToolsManager {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async executeTool(name, args) {
|
|
39
|
+
async executeTool(name, args, signal) {
|
|
40
40
|
const callback = this.callbacks.get(name);
|
|
41
41
|
if (!callback) {
|
|
42
42
|
throw new Error(`Tool not found: ${name}`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
|
-
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
const result = await callback(args, signal);
|
|
48
|
+
signal?.throwIfAborted();
|
|
47
49
|
// For primitive values (numbers, booleans), convert to string
|
|
48
50
|
// For objects/arrays, stringify them
|
|
49
51
|
let textResult;
|
|
@@ -62,6 +64,7 @@ class MCPToolsManager {
|
|
|
62
64
|
}]
|
|
63
65
|
};
|
|
64
66
|
} catch (error) {
|
|
67
|
+
signal?.throwIfAborted();
|
|
65
68
|
log.error(`Error executing tool ${name}:`, error);
|
|
66
69
|
return {
|
|
67
70
|
content: [{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.16",
|
|
4
4
|
"description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -78,6 +78,6 @@
|
|
|
78
78
|
"test:tokens": "mocha test/tokens.test.js --timeout 10000 --require test/setup.js",
|
|
79
79
|
"test:plugins": "mocha test/plugins.test.js --timeout 10000 --require test/setup.js",
|
|
80
80
|
"test:rlm": "mocha plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js",
|
|
81
|
-
"test:offline": "mocha test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js test/grok.test.js test/moderation.test.js test/plugins.test.js plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js"
|
|
81
|
+
"test:offline": "mocha test/abort.test.js test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js test/grok.test.js test/moderation.test.js test/plugins.test.js plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js"
|
|
82
82
|
}
|
|
83
83
|
}
|
package/plugins/rlm/index.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ function sandboxSource(code) {
|
|
|
21
21
|
|
|
22
22
|
function createIsolatedVmSandbox() {
|
|
23
23
|
return {
|
|
24
|
-
async execute({ code, variables, query, limits, timeoutMs }) {
|
|
24
|
+
async execute({ code, variables, query, limits, signal, timeoutMs }) {
|
|
25
|
+
signal?.throwIfAborted();
|
|
25
26
|
if (limits.sandboxMemoryBytes < MINIMUM_ISOLATE_MEMORY_BYTES) {
|
|
26
27
|
throw new TypeError(
|
|
27
28
|
`limits.sandboxMemoryBytes must be at least ${MINIMUM_ISOLATE_MEMORY_BYTES}.`
|
|
@@ -35,6 +36,10 @@ function createIsolatedVmSandbox() {
|
|
|
35
36
|
timedOut = true;
|
|
36
37
|
if (!isolate.isDisposed) isolate.dispose();
|
|
37
38
|
}, timeoutMs);
|
|
39
|
+
const onAbort = () => {
|
|
40
|
+
if (!isolate.isDisposed) isolate.dispose();
|
|
41
|
+
};
|
|
42
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
38
43
|
|
|
39
44
|
try {
|
|
40
45
|
const context = await isolate.createContext();
|
|
@@ -68,6 +73,7 @@ function createIsolatedVmSandbox() {
|
|
|
68
73
|
timeout: timeoutMs
|
|
69
74
|
});
|
|
70
75
|
} catch (error) {
|
|
76
|
+
signal?.throwIfAborted();
|
|
71
77
|
if (memoryError(error)) {
|
|
72
78
|
throw new RlmLimitError(
|
|
73
79
|
'sandboxMemoryBytes',
|
|
@@ -78,6 +84,7 @@ function createIsolatedVmSandbox() {
|
|
|
78
84
|
throw error;
|
|
79
85
|
} finally {
|
|
80
86
|
clearTimeout(timeout);
|
|
87
|
+
signal?.removeEventListener('abort', onAbort);
|
|
81
88
|
if (!isolate.isDisposed) isolate.dispose();
|
|
82
89
|
}
|
|
83
90
|
}
|
|
@@ -13,7 +13,7 @@ const { isPlainObject } = require('./validation');
|
|
|
13
13
|
|
|
14
14
|
function validateSandbox(sandbox) {
|
|
15
15
|
if (!sandbox || typeof sandbox !== 'object' || typeof sandbox.execute !== 'function') {
|
|
16
|
-
throw new TypeError('sandbox must define execute({ code, variables, query, limits }).');
|
|
16
|
+
throw new TypeError('sandbox must define execute({ code, variables, query, limits, signal }).');
|
|
17
17
|
}
|
|
18
18
|
return sandbox;
|
|
19
19
|
}
|
|
@@ -91,9 +91,11 @@ function executionInput(context, configuredVariables) {
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function runWithTimeout(operation, timeoutMs) {
|
|
94
|
+
function runWithTimeout(operation, timeoutMs, signal) {
|
|
95
|
+
signal?.throwIfAborted();
|
|
95
96
|
let timeout;
|
|
96
|
-
|
|
97
|
+
let onAbort;
|
|
98
|
+
const promises = [
|
|
97
99
|
operation(),
|
|
98
100
|
new Promise((_, reject) => {
|
|
99
101
|
timeout = setTimeout(() => reject(new RlmLimitError(
|
|
@@ -101,7 +103,17 @@ function runWithTimeout(operation, timeoutMs) {
|
|
|
101
103
|
'RLM wall-time limit exceeded.'
|
|
102
104
|
)), timeoutMs);
|
|
103
105
|
})
|
|
104
|
-
]
|
|
106
|
+
];
|
|
107
|
+
if (signal) {
|
|
108
|
+
promises.push(new Promise((_, reject) => {
|
|
109
|
+
onAbort = () => reject(signal.reason);
|
|
110
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
return Promise.race(promises).finally(() => {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
if (onAbort) signal.removeEventListener('abort', onAbort);
|
|
116
|
+
});
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } = {}) {
|
|
@@ -122,6 +134,7 @@ function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } =
|
|
|
122
134
|
return {
|
|
123
135
|
name: 'rlm',
|
|
124
136
|
async execute(context) {
|
|
137
|
+
context.signal?.throwIfAborted();
|
|
125
138
|
if (context.request.outputMode === 'stream') {
|
|
126
139
|
throw new Error('RLM streaming is not supported; use a buffered output mode.');
|
|
127
140
|
}
|
|
@@ -145,6 +158,7 @@ function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } =
|
|
|
145
158
|
outputSchema: context.request.config.schema || null
|
|
146
159
|
})
|
|
147
160
|
));
|
|
161
|
+
context.signal?.throwIfAborted();
|
|
148
162
|
state.record('planner', plannerResult, {
|
|
149
163
|
worker: null,
|
|
150
164
|
elapsedMs: Date.now() - plannerStartedAt
|
|
@@ -167,10 +181,13 @@ function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } =
|
|
|
167
181
|
query,
|
|
168
182
|
limits: validatedLimits,
|
|
169
183
|
execution: context.execution,
|
|
184
|
+
signal: context.signal,
|
|
170
185
|
timeoutMs
|
|
171
186
|
}),
|
|
172
|
-
timeoutMs
|
|
187
|
+
timeoutMs,
|
|
188
|
+
context.signal
|
|
173
189
|
);
|
|
190
|
+
context.signal?.throwIfAborted();
|
|
174
191
|
const message = serializeResult(value);
|
|
175
192
|
state.budget.accountFinalOutput(message);
|
|
176
193
|
return {
|
|
@@ -179,6 +196,7 @@ function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } =
|
|
|
179
196
|
rlm: state.diagnostics(context.execution)
|
|
180
197
|
};
|
|
181
198
|
} catch (error) {
|
|
199
|
+
context.signal?.throwIfAborted();
|
|
182
200
|
error.rlm = state.diagnostics(
|
|
183
201
|
context.execution,
|
|
184
202
|
error.limit ? `limit:${error.limit}` : 'error'
|
|
@@ -69,6 +69,29 @@ describe('RLM isolated-vm sandbox', () => {
|
|
|
69
69
|
expect(failure.limit).to.equal('maxWallTimeMs');
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
it('disposes an in-flight isolate and preserves the abort reason', async () => {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const reason = new Error('stop sandbox');
|
|
75
|
+
const execution = createIsolatedVmSandbox().execute({
|
|
76
|
+
code: '(async () => { while (true) {} })()',
|
|
77
|
+
variables: {},
|
|
78
|
+
query: async () => '',
|
|
79
|
+
limits: limits(),
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
timeoutMs: 1000
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
setTimeout(() => controller.abort(reason), 20);
|
|
85
|
+
|
|
86
|
+
let failure;
|
|
87
|
+
try {
|
|
88
|
+
await execution;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
failure = error;
|
|
91
|
+
}
|
|
92
|
+
expect(failure).to.equal(reason);
|
|
93
|
+
});
|
|
94
|
+
|
|
72
95
|
it('rejects memory limits below the isolated-vm minimum', async () => {
|
|
73
96
|
let failure;
|
|
74
97
|
try {
|
package/skills/modelmix/SKILL.md
CHANGED
|
@@ -86,11 +86,11 @@ Chain shorthand methods to attach providers. First model is primary; others are
|
|
|
86
86
|
const model = ModelMix.new()
|
|
87
87
|
.sonnet46() // primary
|
|
88
88
|
.gpt52() // fallback 1
|
|
89
|
-
.
|
|
89
|
+
.gemini38flash() // fallback 2
|
|
90
90
|
.addText("Hello!")
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
If `sonnet46` fails, it automatically tries `gpt52`, then `
|
|
93
|
+
If `sonnet46` fails, it automatically tries `gpt52`, then `gemini38flash`.
|
|
94
94
|
|
|
95
95
|
The equivalent `chain()` form accepts public shortcut names directly in the
|
|
96
96
|
same order. Append `@effort` for a per-model unified effort override (`-1` or
|
|
@@ -99,13 +99,13 @@ provider default when no chain effort is configured:
|
|
|
99
99
|
|
|
100
100
|
```javascript
|
|
101
101
|
const model = ModelMix.new()
|
|
102
|
-
.chain('sonnet46', 'gpt52@20', '
|
|
102
|
+
.chain('sonnet46', 'gpt52@20', 'gemini38flash@-1')
|
|
103
103
|
.addText('Hello!');
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
### Instance plugins
|
|
107
107
|
|
|
108
|
-
Register middleware with `.use({ name, execute })`. Plugins are scoped to the instance, run in registration order, and receive the rendered provider-neutral request. They may edit `context.request`, call `next()`, or return a complete ModelMix result.
|
|
108
|
+
Register middleware with `.use({ name, execute })`. Plugins are scoped to the instance, run in registration order, and receive the rendered provider-neutral request. They may edit `context.request`, call `next()`, or return a complete ModelMix result. `context.signal` is the current execution's `AbortSignal` and is inherited by `context.invoke()` children.
|
|
109
109
|
|
|
110
110
|
```javascript
|
|
111
111
|
model.use({
|
|
@@ -143,7 +143,7 @@ ModelMix.new({ config: { effort: 80 } })
|
|
|
143
143
|
| DeepSeek V4 | off | `low`↑ | `high`↑ | `high`↑ | `max`↑ | — |
|
|
144
144
|
| MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
|
|
145
145
|
|
|
146
|
-
\* GPT-5.6 maps `100` to `max`; 80–99 remains `xhigh`. Qwen 3.8 27B and Flash map 0–39 / 40–79 / 80–100 to `low` / `medium` / `xhigh`; Qwen 3.8 Flash is the managed production version based on Flash-Next. GLM 5.3 and GLM 5.3 Flash require reasoning and map those bands to `low` / `high` / `max`. Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.7 Flash
|
|
146
|
+
\* GPT-5.6 maps `100` to `max`; 80–99 remains `xhigh`. Qwen 3.8 27B and Flash map 0–39 / 40–79 / 80–100 to `low` / `medium` / `xhigh`; Qwen 3.8 Flash is the managed production version based on Flash-Next. GLM 5.3 and GLM 5.3 Flash require reasoning and map those bands to `low` / `high` / `max`. Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.8 Flash and 3.7 Flash support only `low` / `medium` / `high`, so the first two bands clamp to `low`; `-1` keeps their native `medium` default. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. Anthropic: adaptive + `output_config.effort` on Claude 5 / Fable / Opus 4.6+ / Sonnet 4.6+; Sonnet 4.5 / Haiku 4.5 use `thinking.type=enabled` + `budget_tokens`. Grok 4.6 maps 0–39 / 40–59 / 60–79 / 80–100 to `low` / `medium` / `high` / `xhigh`; without effort it uses native `high`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. Former `*think()` methods are removed — use `.effort(n).<model>()`. Kimi: `kimiK25()` / `kimiK26()`. Grok 4.20: `.grok420()` non-reasoning; `.effort(20+|-1).grok420()` selects reasoning.
|
|
147
147
|
|
|
148
148
|
## Available Model Shorthands
|
|
149
149
|
|
|
@@ -163,7 +163,7 @@ Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.ef
|
|
|
163
163
|
`fable51()` registers `claude-fable-5-1` through Anthropic by default. Pass `mix: { openrouter: true }` to append `anthropic/claude-fable-5.1` as its fallback.
|
|
164
164
|
|
|
165
165
|
### Google
|
|
166
|
-
`gemini31pro()` `gemini37flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()`
|
|
166
|
+
`gemini31pro()` `gemini38flash()` `gemini37flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()`
|
|
167
167
|
|
|
168
168
|
### Grok
|
|
169
169
|
`grok46()` `grok45()` `grok43()` `grok420multiAgent()` `grok420()`
|
|
@@ -332,6 +332,24 @@ const raw = await ModelMix.new()
|
|
|
332
332
|
// raw.message, raw.think, raw.tokens, raw.toolCalls, raw.response
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
### Cancel one execution
|
|
336
|
+
|
|
337
|
+
Pass an `AbortSignal` directly to the terminal method:
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
const controller = new AbortController();
|
|
341
|
+
const pending = model.message(controller.signal);
|
|
342
|
+
controller.abort(new Error('Request cancelled'));
|
|
343
|
+
await pending;
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The other forms are `raw(signal)`, `stream(callback, signal)`,
|
|
347
|
+
`block(options, signal)`, and `json(example, description, jsonOptions, signal)`.
|
|
348
|
+
For `execute()`, use the top-level `{ signal }` field. Never place `signal` in
|
|
349
|
+
`config` or `options`; ModelMix rejects those reusable/payload-bearing locations.
|
|
350
|
+
Plugins read `context.signal`, child invocations inherit it, and local tool
|
|
351
|
+
callbacks receive it as their second argument.
|
|
352
|
+
|
|
335
353
|
### Access full response with lastRaw
|
|
336
354
|
|
|
337
355
|
After calling `message()`, `json()`, `block()`, or `stream()`, use `lastRaw` to access the complete response:
|
|
@@ -467,7 +485,7 @@ Omit all weights for equal probabilities. Otherwise every option needs a positiv
|
|
|
467
485
|
const pool = ModelMix.new({ config: { roundRobin: true } })
|
|
468
486
|
.gpt5mini()
|
|
469
487
|
.sonnet45()
|
|
470
|
-
.
|
|
488
|
+
.gemini38flash();
|
|
471
489
|
|
|
472
490
|
const r1 = await pool.new().addText("Request 1").message();
|
|
473
491
|
const r2 = await pool.new().addText("Request 2").message();
|
|
@@ -614,12 +632,12 @@ const model = ModelMix.new({
|
|
|
614
632
|
| `.assign({})` | `this` | Assign EJS template data |
|
|
615
633
|
| `.assignKey(key, value)` | `this` | Assign one EJS template-data value |
|
|
616
634
|
| `.assignKeyFromFile(key, path)` | `this` | Assign the rendered output of an EJS file to one key |
|
|
617
|
-
| `.message()` | `Promise<string>` | Get text response |
|
|
618
|
-
| `.json(example, desc?, opts?)` | `Promise<object\|array>` | Get structured JSON |
|
|
619
|
-
| `.raw()` | `Promise<{message, think, toolCalls, tokens, response}>` | Full response |
|
|
635
|
+
| `.message(signal?)` | `Promise<string>` | Get text response |
|
|
636
|
+
| `.json(example, desc?, opts?, signal?)` | `Promise<object\|array>` | Get structured JSON |
|
|
637
|
+
| `.raw(signal?)` | `Promise<{message, think, toolCalls, tokens, response}>` | Full response |
|
|
620
638
|
| `.lastRaw` | `object \| null` | Full response from last call |
|
|
621
|
-
| `.stream(callback)` | `Promise` | Stream response |
|
|
622
|
-
| `.block({addSystemExtra?})` | `Promise<string>` | Extract code block from response |
|
|
639
|
+
| `.stream(callback, signal?)` | `Promise` | Stream response |
|
|
640
|
+
| `.block({addSystemExtra?}, signal?)` | `Promise<string>` | Extract code block from response |
|
|
623
641
|
| `.addMCP(package)` | `Promise` | Add MCP server tools |
|
|
624
642
|
| `.addTool(def, callback)` | `this` | Register custom local tool |
|
|
625
643
|
| `.addTools([{tool, callback}])` | `this` | Register multiple tools |
|