modelmix 5.1.10 → 5.1.15
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 +42 -10
- package/demo/free.js +1 -3
- package/demo/short.js +15 -13
- package/effort.js +13 -7
- package/http-client.js +6 -6
- package/index.d.ts +13 -6
- package/index.js +204 -144
- package/lib/abort-signal.js +57 -0
- package/lib/model-chain.js +2 -2
- package/lib/provider-api-key.js +9 -0
- package/lib/providers/anthropic.js +24 -22
- package/lib/providers/base.js +21 -28
- package/lib/providers/google.js +12 -8
- package/lib/providers/openai-compatible.js +19 -48
- package/lib/providers/openai-options.js +16 -0
- package/lib/providers/openai.js +48 -47
- package/lib/providers.js +5 -2
- package/lib/token-usage.js +19 -4
- package/mcp-tools.js +5 -2
- package/package.json +3 -2
- package/plugins/rlm/index.d.ts +1 -0
- package/plugins/rlm/lib/budget.js +2 -7
- package/plugins/rlm/lib/isolated-vm-sandbox.js +8 -1
- package/plugins/rlm/lib/planner-prompt.js +1 -7
- package/plugins/rlm/lib/plugin.js +24 -11
- package/plugins/rlm/lib/validation.js +14 -0
- package/plugins/rlm/lib/variable-descriptors.js +1 -5
- package/plugins/rlm/lib/worker-catalog.js +1 -5
- package/plugins/rlm/test/isolated-vm-sandbox.test.js +23 -0
- package/pnpm-workspace.yaml +10 -5
- package/schema.js +0 -28
- package/skills/modelmix/SKILL.md +34 -12
- package/test/abort.test.js +517 -0
- package/test/anthropic.test.js +77 -1
- package/test/bottleneck.test.js +2 -2
- package/test/effort.test.js +24 -1
- package/test/fallback.test.js +101 -1
- package/test/glm.test.js +2 -6
- package/test/kimi.test.js +2 -6
- package/test/live.test.js +10 -5
- package/test/muse.test.js +24 -7
- package/test/provider-expansion.test.js +17 -0
- package/test/public-api.test.js +2 -2
- package/test/qwen.test.js +2 -3
- package/test/tokens.test.js +23 -8
package/lib/providers/openai.js
CHANGED
|
@@ -4,38 +4,35 @@ const {
|
|
|
4
4
|
normalizeContentCache,
|
|
5
5
|
stripContentCacheMetadata
|
|
6
6
|
} = require('../content-cache');
|
|
7
|
+
const { requireProviderApiKey } = require('../provider-api-key');
|
|
8
|
+
const { normalizeOpenAIOptions } = require('./openai-options');
|
|
9
|
+
const {
|
|
10
|
+
throwIfAborted,
|
|
11
|
+
validateProviderExecution
|
|
12
|
+
} = require('../abort-signal');
|
|
7
13
|
|
|
8
14
|
function createOpenAIProviders({
|
|
9
15
|
ModelMix,
|
|
10
16
|
MixCustom,
|
|
11
17
|
MixOpenAI,
|
|
12
|
-
MixModeration
|
|
18
|
+
MixModeration,
|
|
19
|
+
rejectsAnthropicSamplingParams
|
|
13
20
|
}) {
|
|
14
21
|
const WebSocket = require('ws');
|
|
15
22
|
|
|
16
23
|
class MixOpenAIResponses extends MixOpenAI {
|
|
17
|
-
async create({ config = {}, options = {} } = {}) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (options.model?.startsWith('o')) {
|
|
21
|
-
delete options.max_tokens;
|
|
22
|
-
delete options.temperature;
|
|
23
|
-
}
|
|
24
|
-
if (options.model?.includes('gpt-5')) {
|
|
25
|
-
if (options.max_tokens) {
|
|
26
|
-
options.max_completion_tokens = options.max_tokens;
|
|
27
|
-
delete options.max_tokens;
|
|
28
|
-
}
|
|
29
|
-
delete options.temperature;
|
|
30
|
-
}
|
|
31
|
-
|
|
24
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
25
|
+
validateProviderExecution(this, { config, options, signal });
|
|
26
|
+
normalizeOpenAIOptions(options);
|
|
32
27
|
const responsesUrl = this.config.url.replace('/chat/completions', '/responses');
|
|
33
28
|
const request = MixOpenAIResponses.buildResponsesRequest(options, config);
|
|
34
29
|
const response = await fetchJsonResponse(responsesUrl, {
|
|
35
30
|
method: 'POST',
|
|
36
31
|
headers: this.headers,
|
|
37
|
-
body: JSON.stringify(request)
|
|
32
|
+
body: JSON.stringify(request),
|
|
33
|
+
signal
|
|
38
34
|
});
|
|
35
|
+
throwIfAborted(signal);
|
|
39
36
|
|
|
40
37
|
return MixOpenAIResponses.processResponsesResponse(response);
|
|
41
38
|
}
|
|
@@ -246,10 +243,7 @@ function createOpenAIProviders({
|
|
|
246
243
|
|
|
247
244
|
class MixOpenAIModeration extends MixModeration {
|
|
248
245
|
getDefaultConfig(customConfig) {
|
|
249
|
-
const apiKey = customConfig
|
|
250
|
-
if (!apiKey) {
|
|
251
|
-
throw new Error('OpenAI API key not found. Please provide it in config or set OPENAI_API_KEY environment variable.');
|
|
252
|
-
}
|
|
246
|
+
const apiKey = requireProviderApiKey(customConfig, 'OPENAI_API_KEY', 'OpenAI');
|
|
253
247
|
|
|
254
248
|
return super.getDefaultConfig({
|
|
255
249
|
url: 'https://api.openai.com/v1/moderations',
|
|
@@ -258,7 +252,8 @@ function createOpenAIProviders({
|
|
|
258
252
|
});
|
|
259
253
|
}
|
|
260
254
|
|
|
261
|
-
async create({ config = {}, options = {} } = {}) {
|
|
255
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
256
|
+
validateProviderExecution(this, { config, options, signal });
|
|
262
257
|
if (options.stream) {
|
|
263
258
|
throw new Error('Stream is not supported for OpenAI moderation');
|
|
264
259
|
}
|
|
@@ -267,8 +262,10 @@ function createOpenAIProviders({
|
|
|
267
262
|
const response = await fetchJsonResponse(this.config.url, {
|
|
268
263
|
method: 'POST',
|
|
269
264
|
headers: this.headers,
|
|
270
|
-
body: JSON.stringify({ model: options.model, input })
|
|
265
|
+
body: JSON.stringify({ model: options.model, input }),
|
|
266
|
+
signal
|
|
271
267
|
});
|
|
268
|
+
throwIfAborted(signal);
|
|
272
269
|
|
|
273
270
|
return {
|
|
274
271
|
moderation: response.data.results,
|
|
@@ -356,19 +353,9 @@ function createOpenAIProviders({
|
|
|
356
353
|
});
|
|
357
354
|
}
|
|
358
355
|
|
|
359
|
-
async create({ config = {}, options = {} } = {}) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
delete options.temperature;
|
|
363
|
-
}
|
|
364
|
-
if (options.model?.includes('gpt-5')) {
|
|
365
|
-
if (options.max_tokens) {
|
|
366
|
-
options.max_completion_tokens = options.max_tokens;
|
|
367
|
-
delete options.max_tokens;
|
|
368
|
-
}
|
|
369
|
-
delete options.temperature;
|
|
370
|
-
}
|
|
371
|
-
|
|
356
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
357
|
+
validateProviderExecution(this, { config, options, signal });
|
|
358
|
+
normalizeOpenAIOptions(options);
|
|
372
359
|
const mergedConfig = { ...this.config, ...config };
|
|
373
360
|
const realtimeUrl = `${mergedConfig.realtimeUrl}?model=${encodeURIComponent(options.model)}`;
|
|
374
361
|
const timeoutMs = mergedConfig.websocketTimeoutMs || 120000;
|
|
@@ -396,7 +383,18 @@ function createOpenAIProviders({
|
|
|
396
383
|
});
|
|
397
384
|
}, timeoutMs);
|
|
398
385
|
|
|
399
|
-
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 });
|
|
400
398
|
|
|
401
399
|
ws.on('open', () => {
|
|
402
400
|
const session = {
|
|
@@ -556,10 +554,7 @@ function createOpenAIProviders({
|
|
|
556
554
|
|
|
557
555
|
class MixOpenRouter extends MixOpenAI {
|
|
558
556
|
getDefaultConfig(customConfig) {
|
|
559
|
-
const apiKey = customConfig
|
|
560
|
-
if (!apiKey) {
|
|
561
|
-
throw new Error('OpenRouter API key not found. Please provide it in config or set OPENROUTER_API_KEY environment variable.');
|
|
562
|
-
}
|
|
557
|
+
const apiKey = requireProviderApiKey(customConfig, 'OPENROUTER_API_KEY', 'OpenRouter');
|
|
563
558
|
|
|
564
559
|
return MixCustom.prototype.getDefaultConfig.call(this, {
|
|
565
560
|
url: 'https://openrouter.ai/api/v1/chat/completions',
|
|
@@ -567,14 +562,20 @@ function createOpenAIProviders({
|
|
|
567
562
|
...customConfig
|
|
568
563
|
});
|
|
569
564
|
}
|
|
565
|
+
|
|
566
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
567
|
+
if (rejectsAnthropicSamplingParams(options.model)) {
|
|
568
|
+
delete options.temperature;
|
|
569
|
+
delete options.top_p;
|
|
570
|
+
delete options.top_k;
|
|
571
|
+
}
|
|
572
|
+
return super.create({ config, options, signal });
|
|
573
|
+
}
|
|
570
574
|
}
|
|
571
575
|
|
|
572
576
|
class MixKimi extends MixOpenAI {
|
|
573
577
|
getDefaultConfig(customConfig) {
|
|
574
|
-
const apiKey = customConfig
|
|
575
|
-
if (!apiKey) {
|
|
576
|
-
throw new Error('Moonshot API key not found. Please provide it in config or set MOONSHOT_API_KEY environment variable.');
|
|
577
|
-
}
|
|
578
|
+
const apiKey = requireProviderApiKey(customConfig, 'MOONSHOT_API_KEY', 'Moonshot');
|
|
578
579
|
|
|
579
580
|
return MixCustom.prototype.getDefaultConfig.call(this, {
|
|
580
581
|
url: 'https://api.moonshot.ai/v1/chat/completions',
|
|
@@ -583,7 +584,7 @@ function createOpenAIProviders({
|
|
|
583
584
|
});
|
|
584
585
|
}
|
|
585
586
|
|
|
586
|
-
async create({ config = {}, options = {} } = {}) {
|
|
587
|
+
async create({ config = {}, options = {}, signal } = {}) {
|
|
587
588
|
if (Object.hasOwn(options, 'max_tokens')) {
|
|
588
589
|
options.max_completion_tokens = options.max_tokens;
|
|
589
590
|
delete options.max_tokens;
|
|
@@ -595,7 +596,7 @@ function createOpenAIProviders({
|
|
|
595
596
|
delete options.presence_penalty;
|
|
596
597
|
delete options.frequency_penalty;
|
|
597
598
|
|
|
598
|
-
return super.create({ config, options });
|
|
599
|
+
return super.create({ config, options, signal });
|
|
599
600
|
}
|
|
600
601
|
|
|
601
602
|
extractDelta(data) {
|
package/lib/providers.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
const createBaseProviders = require('./providers/base');
|
|
2
2
|
const createOpenAIProviders = require('./providers/openai');
|
|
3
|
-
const
|
|
3
|
+
const {
|
|
4
|
+
createAnthropicProviders,
|
|
5
|
+
rejectsAnthropicSamplingParams
|
|
6
|
+
} = require('./providers/anthropic');
|
|
4
7
|
const createCompatibleProviders = require('./providers/openai-compatible');
|
|
5
8
|
const createGoogleProviders = require('./providers/google');
|
|
6
9
|
|
|
7
10
|
function createProviders({ ModelMix, log }) {
|
|
8
11
|
const base = createBaseProviders({ ModelMix });
|
|
9
|
-
const openai = createOpenAIProviders({ ModelMix, ...base });
|
|
12
|
+
const openai = createOpenAIProviders({ ModelMix, rejectsAnthropicSamplingParams, ...base });
|
|
10
13
|
const anthropic = createAnthropicProviders({ ModelMix, MixCustom: base.MixCustom, log });
|
|
11
14
|
const compatible = createCompatibleProviders({
|
|
12
15
|
MixCustom: base.MixCustom,
|
package/lib/token-usage.js
CHANGED
|
@@ -35,6 +35,8 @@ const MODEL_PRICING = {
|
|
|
35
35
|
'gpt-5.3-codex': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
36
36
|
'gpt-5.2': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
37
37
|
'gpt-5.2-chat-latest': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
38
|
+
'gpt-5.3-chat-latest': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
39
|
+
'openai/gpt-5.3-chat': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
38
40
|
'gpt-5.1': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
39
41
|
'gpt-5': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
40
42
|
'gpt-5-mini': { input: 0.25, cachedInput: 0.025, output: 2.00 },
|
|
@@ -47,6 +49,8 @@ const MODEL_PRICING = {
|
|
|
47
49
|
'gpt-oss-120b': { input: 0.15, output: 0.60 },
|
|
48
50
|
'accounts/fireworks/models/gpt-oss-120b': { input: 0.15, cachedInput: 0.014, output: 0.60 },
|
|
49
51
|
// Anthropic
|
|
52
|
+
'claude-fable-5-1': { input: 10.00, cachedInput: 0.25, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
|
|
53
|
+
'anthropic/claude-fable-5.1': { input: 10.00, cachedInput: 0.25, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
|
|
50
54
|
'claude-fable-5': { input: 10.00, cachedInput: 1.00, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
|
|
51
55
|
'claude-opus-5': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
52
56
|
'claude-sonnet-5': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
@@ -75,10 +79,11 @@ const MODEL_PRICING = {
|
|
|
75
79
|
'grok-4.20-0309': { input: 1.25, output: 2.50 },
|
|
76
80
|
'grok-4.20-0309-reasoning': { input: 1.25, output: 2.50 },
|
|
77
81
|
'grok-4.20-0309-non-reasoning': { input: 1.25, output: 2.50 },
|
|
78
|
-
// Muse
|
|
82
|
+
// Muse
|
|
79
83
|
'meta/muse-glimmer-30b': { input: 0.35, cachedInput: 0.04, output: 1.50 },
|
|
80
84
|
'accounts/fireworks/models/muse-glimmer-30b': { input: 0.35, cachedInput: 0.04, output: 1.50 },
|
|
81
85
|
'meta-models/Muse-Glimmer-30B': { input: 0.35, cachedInput: 0.04, output: 1.50 },
|
|
86
|
+
'meta/muse-spark-1.2-contributor': { input: 0.10, cachedInput: 0.002, output: 0.20 },
|
|
82
87
|
// Fireworks
|
|
83
88
|
'accounts/fireworks/models/deepseek-v4-flash': { input: 0.14, output: 0.28 },
|
|
84
89
|
'accounts/fireworks/models/deepseek-v4-pro': { input: 1.74, output: 3.48 },
|
|
@@ -200,7 +205,7 @@ function normalizeTokenUsage({ input = 0, output = 0, thinking = 0, total, cache
|
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
function calculateCostBreakdown(modelKey, tokens) {
|
|
203
|
-
const pricing =
|
|
208
|
+
const pricing = getModelPricing(modelKey);
|
|
204
209
|
if (!pricing) return normalizeTokenUsage().costBreakdown;
|
|
205
210
|
|
|
206
211
|
const normalized = normalizeTokenUsage(tokens);
|
|
@@ -249,7 +254,7 @@ function calculateCostBreakdown(modelKey, tokens) {
|
|
|
249
254
|
}
|
|
250
255
|
|
|
251
256
|
function calculateCacheMetrics(modelKey, tokens) {
|
|
252
|
-
const pricing =
|
|
257
|
+
const pricing = getModelPricing(modelKey);
|
|
253
258
|
const emptyMetrics = {
|
|
254
259
|
cacheSavings: 0,
|
|
255
260
|
cacheWritePremium: 0,
|
|
@@ -296,7 +301,17 @@ function calculateCost(modelKey, tokens) {
|
|
|
296
301
|
}
|
|
297
302
|
|
|
298
303
|
function hasModelPricing(modelKey) {
|
|
299
|
-
return
|
|
304
|
+
return getModelPricing(modelKey) !== undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getModelPricing(modelKey) {
|
|
308
|
+
if (Object.prototype.hasOwnProperty.call(MODEL_PRICING, modelKey)) {
|
|
309
|
+
return MODEL_PRICING[modelKey];
|
|
310
|
+
}
|
|
311
|
+
if (typeof modelKey === 'string' && modelKey.startsWith('openai/')) {
|
|
312
|
+
return MODEL_PRICING[modelKey.slice('openai/'.length)];
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
function extractCacheTokens(usage = {}) {
|
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.15",
|
|
4
4
|
"description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"gpt5",
|
|
28
28
|
"opus50",
|
|
29
29
|
"opus5",
|
|
30
|
+
"fable51",
|
|
30
31
|
"fable50",
|
|
31
32
|
"fable",
|
|
32
33
|
"sonnet",
|
|
@@ -77,6 +78,6 @@
|
|
|
77
78
|
"test:tokens": "mocha test/tokens.test.js --timeout 10000 --require test/setup.js",
|
|
78
79
|
"test:plugins": "mocha test/plugins.test.js --timeout 10000 --require test/setup.js",
|
|
79
80
|
"test:rlm": "mocha plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js",
|
|
80
|
-
"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"
|
|
81
82
|
}
|
|
82
83
|
}
|
package/plugins/rlm/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { positiveInteger } = require('./validation');
|
|
2
|
+
|
|
1
3
|
class RlmLimitError extends Error {
|
|
2
4
|
constructor(limit, message) {
|
|
3
5
|
super(message);
|
|
@@ -6,13 +8,6 @@ class RlmLimitError extends Error {
|
|
|
6
8
|
}
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
function positiveInteger(value, name) {
|
|
10
|
-
if (!Number.isInteger(value) || value <= 0) {
|
|
11
|
-
throw new TypeError(`${name} must be a positive integer.`);
|
|
12
|
-
}
|
|
13
|
-
return value;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
function validateRuntimeLimits(limits) {
|
|
17
12
|
if (!limits || typeof limits !== 'object' || Array.isArray(limits)) {
|
|
18
13
|
throw new TypeError('limits must be a plain object.');
|
|
@@ -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
|
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { describeVariables } = require('./variable-descriptors');
|
|
3
|
+
const { positiveInteger } = require('./validation');
|
|
3
4
|
|
|
4
5
|
const PLANNER_SYSTEM_TEMPLATE = path.resolve(__dirname, '../prompts/planner.md');
|
|
5
6
|
|
|
6
|
-
function positiveInteger(value, name) {
|
|
7
|
-
if (!Number.isInteger(value) || value <= 0) {
|
|
8
|
-
throw new TypeError(`${name} must be a positive integer.`);
|
|
9
|
-
}
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
function planningHint(name, descriptor, maxQueryBytes) {
|
|
14
8
|
const payloadBytes = descriptor.utf8Bytes ?? descriptor.estimatedBytes;
|
|
15
9
|
if (payloadBytes <= maxQueryBytes) {
|
|
@@ -9,16 +9,11 @@ const {
|
|
|
9
9
|
sumTokens
|
|
10
10
|
} = require('./runtime');
|
|
11
11
|
const { createWorkerCatalog } = require('./worker-catalog');
|
|
12
|
-
|
|
13
|
-
function isPlainObject(value) {
|
|
14
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
15
|
-
const prototype = Object.getPrototypeOf(value);
|
|
16
|
-
return prototype === Object.prototype || prototype === null;
|
|
17
|
-
}
|
|
12
|
+
const { isPlainObject } = require('./validation');
|
|
18
13
|
|
|
19
14
|
function validateSandbox(sandbox) {
|
|
20
15
|
if (!sandbox || typeof sandbox !== 'object' || typeof sandbox.execute !== 'function') {
|
|
21
|
-
throw new TypeError('sandbox must define execute({ code, variables, query, limits }).');
|
|
16
|
+
throw new TypeError('sandbox must define execute({ code, variables, query, limits, signal }).');
|
|
22
17
|
}
|
|
23
18
|
return sandbox;
|
|
24
19
|
}
|
|
@@ -96,9 +91,11 @@ function executionInput(context, configuredVariables) {
|
|
|
96
91
|
};
|
|
97
92
|
}
|
|
98
93
|
|
|
99
|
-
function runWithTimeout(operation, timeoutMs) {
|
|
94
|
+
function runWithTimeout(operation, timeoutMs, signal) {
|
|
95
|
+
signal?.throwIfAborted();
|
|
100
96
|
let timeout;
|
|
101
|
-
|
|
97
|
+
let onAbort;
|
|
98
|
+
const promises = [
|
|
102
99
|
operation(),
|
|
103
100
|
new Promise((_, reject) => {
|
|
104
101
|
timeout = setTimeout(() => reject(new RlmLimitError(
|
|
@@ -106,7 +103,17 @@ function runWithTimeout(operation, timeoutMs) {
|
|
|
106
103
|
'RLM wall-time limit exceeded.'
|
|
107
104
|
)), timeoutMs);
|
|
108
105
|
})
|
|
109
|
-
]
|
|
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
|
+
});
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } = {}) {
|
|
@@ -127,6 +134,7 @@ function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } =
|
|
|
127
134
|
return {
|
|
128
135
|
name: 'rlm',
|
|
129
136
|
async execute(context) {
|
|
137
|
+
context.signal?.throwIfAborted();
|
|
130
138
|
if (context.request.outputMode === 'stream') {
|
|
131
139
|
throw new Error('RLM streaming is not supported; use a buffered output mode.');
|
|
132
140
|
}
|
|
@@ -150,6 +158,7 @@ function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } =
|
|
|
150
158
|
outputSchema: context.request.config.schema || null
|
|
151
159
|
})
|
|
152
160
|
));
|
|
161
|
+
context.signal?.throwIfAborted();
|
|
153
162
|
state.record('planner', plannerResult, {
|
|
154
163
|
worker: null,
|
|
155
164
|
elapsedMs: Date.now() - plannerStartedAt
|
|
@@ -172,10 +181,13 @@ function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } =
|
|
|
172
181
|
query,
|
|
173
182
|
limits: validatedLimits,
|
|
174
183
|
execution: context.execution,
|
|
184
|
+
signal: context.signal,
|
|
175
185
|
timeoutMs
|
|
176
186
|
}),
|
|
177
|
-
timeoutMs
|
|
187
|
+
timeoutMs,
|
|
188
|
+
context.signal
|
|
178
189
|
);
|
|
190
|
+
context.signal?.throwIfAborted();
|
|
179
191
|
const message = serializeResult(value);
|
|
180
192
|
state.budget.accountFinalOutput(message);
|
|
181
193
|
return {
|
|
@@ -184,6 +196,7 @@ function rlm({ maxDepth, variables = {}, documents, workers, limits, sandbox } =
|
|
|
184
196
|
rlm: state.diagnostics(context.execution)
|
|
185
197
|
};
|
|
186
198
|
} catch (error) {
|
|
199
|
+
context.signal?.throwIfAborted();
|
|
187
200
|
error.rlm = state.diagnostics(
|
|
188
201
|
context.execution,
|
|
189
202
|
error.limit ? `limit:${error.limit}` : 'error'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
3
|
+
const prototype = Object.getPrototypeOf(value);
|
|
4
|
+
return prototype === Object.prototype || prototype === null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function positiveInteger(value, name) {
|
|
8
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
9
|
+
throw new TypeError(`${name} must be a positive integer.`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { isPlainObject, positiveInteger };
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
3
|
-
const prototype = Object.getPrototypeOf(value);
|
|
4
|
-
return prototype === Object.prototype || prototype === null;
|
|
5
|
-
}
|
|
1
|
+
const { isPlainObject } = require('./validation');
|
|
6
2
|
|
|
7
3
|
function roundAverage(total, count) {
|
|
8
4
|
return count === 0 ? 0 : Number((total / count).toFixed(2));
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
3
|
-
const prototype = Object.getPrototypeOf(value);
|
|
4
|
-
return prototype === Object.prototype || prototype === null;
|
|
5
|
-
}
|
|
1
|
+
const { isPlainObject } = require('./validation');
|
|
6
2
|
|
|
7
3
|
function finiteRating(value, path) {
|
|
8
4
|
if (!Number.isFinite(value) || value < 0) {
|
|
@@ -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/pnpm-workspace.yaml
CHANGED
|
@@ -6,11 +6,16 @@ allowBuilds:
|
|
|
6
6
|
|
|
7
7
|
minimumReleaseAgeExclude:
|
|
8
8
|
- ws@8.21.0
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
9
|
+
- fast-uri@3.1.5
|
|
10
|
+
- hono@4.12.34
|
|
11
|
+
- ip-address@10.3.1
|
|
12
|
+
- js-yaml@4.3.1
|
|
13
|
+
- brace-expansion@5.0.9
|
|
12
14
|
overrides:
|
|
13
|
-
brace-expansion
|
|
15
|
+
brace-expansion: 5.0.9
|
|
14
16
|
diff: 8.0.4
|
|
15
|
-
|
|
17
|
+
fast-uri: 3.1.5
|
|
18
|
+
hono: 4.12.34
|
|
19
|
+
ip-address: 10.3.1
|
|
20
|
+
js-yaml: 4.3.1
|
|
16
21
|
serialize-javascript: 7.0.5
|
package/schema.js
CHANGED
|
@@ -122,31 +122,3 @@ function generateJsonSchema(example, descriptions = {}) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
module.exports = generateJsonSchema;
|
|
125
|
-
|
|
126
|
-
// const example = {
|
|
127
|
-
// name: 'Alice',
|
|
128
|
-
// age: 30,
|
|
129
|
-
// email: 'alice@example.com',
|
|
130
|
-
// birthDate: '1990-01-01',
|
|
131
|
-
// isAdmin: false,
|
|
132
|
-
// preferences: {
|
|
133
|
-
// theme: 'dark',
|
|
134
|
-
// notifications: true
|
|
135
|
-
// },
|
|
136
|
-
// tags: ['admin', 'user']
|
|
137
|
-
// };
|
|
138
|
-
|
|
139
|
-
// const descriptions = {
|
|
140
|
-
// name: 'Full name of the user',
|
|
141
|
-
// age: 'Age must be 0 or greater',
|
|
142
|
-
// email: 'User email address',
|
|
143
|
-
// // birthDate: 'User birth date in YYYY-MM-DD format',
|
|
144
|
-
// preferences: {
|
|
145
|
-
// theme: 'Theme preference (light/dark)',
|
|
146
|
-
// notifications: 'Whether notifications are enabled'
|
|
147
|
-
// }
|
|
148
|
-
// };
|
|
149
|
-
|
|
150
|
-
// const schema = generateJsonSchema(example);
|
|
151
|
-
|
|
152
|
-
// console.log(JSON.stringify(schema, null, 2));
|