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/index.js
CHANGED
|
@@ -12,6 +12,13 @@ const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
|
|
|
12
12
|
const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
13
13
|
const { MCPToolsManager } = require('./mcp-tools');
|
|
14
14
|
const { fetchBinaryResponse } = require('./http-client');
|
|
15
|
+
const {
|
|
16
|
+
assertAbortSignal,
|
|
17
|
+
assertNoStoredSignal,
|
|
18
|
+
raceWithSignal,
|
|
19
|
+
sleepWithSignal,
|
|
20
|
+
throwIfAborted
|
|
21
|
+
} = require('./lib/abort-signal');
|
|
15
22
|
const { isPlainObject } = require('./lib/object-utils');
|
|
16
23
|
const { normalizeContentCache } = require('./lib/content-cache');
|
|
17
24
|
const tokenUsage = require('./lib/token-usage');
|
|
@@ -59,10 +66,6 @@ function getErrorStatusCode(error) {
|
|
|
59
66
|
return error?.statusCode ?? error?.response?.status ?? error?.response?.statusCode ?? null;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
function sleep(ms) {
|
|
63
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
69
|
function clonePluginValue(value, seen = new WeakMap()) {
|
|
67
70
|
if (value === null || typeof value !== 'object') return value;
|
|
68
71
|
if (Buffer.isBuffer(value)) return Buffer.from(value);
|
|
@@ -96,6 +99,8 @@ function validatePluginResult(result, pluginName) {
|
|
|
96
99
|
class ModelMix {
|
|
97
100
|
|
|
98
101
|
constructor({ options = {}, config = {}, mix = {} } = {}) {
|
|
102
|
+
assertNoStoredSignal(options, 'options');
|
|
103
|
+
assertNoStoredSignal(config, 'config');
|
|
99
104
|
this.models = [];
|
|
100
105
|
this.messages = [];
|
|
101
106
|
this.tools = {};
|
|
@@ -144,7 +149,7 @@ class ModelMix {
|
|
|
144
149
|
if (this.config.effort !== undefined && this.config.effort !== null) {
|
|
145
150
|
this.config.effort = normalizeEffort(this.config.effort);
|
|
146
151
|
}
|
|
147
|
-
const freeMix = { openrouter:
|
|
152
|
+
const freeMix = { openrouter: false, cerebras: true, groq: true, together: false, lambda: false };
|
|
148
153
|
this.mix = { ...freeMix, ...mix };
|
|
149
154
|
|
|
150
155
|
this.limiter = new Bottleneck(this.config.bottleneck);
|
|
@@ -255,13 +260,16 @@ class ModelMix {
|
|
|
255
260
|
: this.plugins.filter(plugin => !uniqueNames.has(plugin.name));
|
|
256
261
|
}
|
|
257
262
|
|
|
258
|
-
async _invokeChild(input, parentExecution) {
|
|
263
|
+
async _invokeChild(input, parentExecution, signal) {
|
|
259
264
|
if (!isPlainObject(input)) {
|
|
260
265
|
throw new TypeError('Child invocation must be a plain object.');
|
|
261
266
|
}
|
|
262
267
|
if (input.history !== undefined && input.history !== false) {
|
|
263
268
|
throw new TypeError('Child invocations currently require history: false.');
|
|
264
269
|
}
|
|
270
|
+
if (Object.prototype.hasOwnProperty.call(input, 'signal')) {
|
|
271
|
+
throw new TypeError('Child invocations inherit the parent AbortSignal and cannot override it.');
|
|
272
|
+
}
|
|
265
273
|
|
|
266
274
|
const {
|
|
267
275
|
system,
|
|
@@ -276,6 +284,9 @@ class ModelMix {
|
|
|
276
284
|
plugins = 'inherit',
|
|
277
285
|
outputMode = 'raw'
|
|
278
286
|
} = input;
|
|
287
|
+
assertNoStoredSignal(options, 'options');
|
|
288
|
+
assertNoStoredSignal(config, 'config');
|
|
289
|
+
throwIfAborted(signal);
|
|
279
290
|
if (!Array.isArray(messages)) {
|
|
280
291
|
throw new TypeError('Child invocation messages must be an array.');
|
|
281
292
|
}
|
|
@@ -318,6 +329,7 @@ class ModelMix {
|
|
|
318
329
|
};
|
|
319
330
|
const result = await child.execute({
|
|
320
331
|
outputMode,
|
|
332
|
+
signal,
|
|
321
333
|
_executionMetadata: execution
|
|
322
334
|
});
|
|
323
335
|
return { ...result, execution };
|
|
@@ -421,6 +433,9 @@ class ModelMix {
|
|
|
421
433
|
|
|
422
434
|
attach(key, provider) {
|
|
423
435
|
|
|
436
|
+
assertNoStoredSignal(provider?.options, 'provider.options');
|
|
437
|
+
assertNoStoredSignal(provider?.config, 'provider.config');
|
|
438
|
+
|
|
424
439
|
if (this.models.some(model => model.key === key
|
|
425
440
|
&& model.provider.constructor === provider.constructor)) {
|
|
426
441
|
return this;
|
|
@@ -434,47 +449,59 @@ class ModelMix {
|
|
|
434
449
|
return this;
|
|
435
450
|
}
|
|
436
451
|
|
|
437
|
-
|
|
438
|
-
|
|
452
|
+
_attachOpenAIWithOpenRouter(officialKey, Provider, {
|
|
453
|
+
options = {},
|
|
454
|
+
config = {},
|
|
455
|
+
mix = {},
|
|
456
|
+
openRouterKey = `openai/${officialKey}`
|
|
457
|
+
} = {}) {
|
|
458
|
+
mix = { ...this.mix, ...mix };
|
|
459
|
+
this.attach(officialKey, new Provider({ options, config }));
|
|
460
|
+
if (mix.openrouter) this.attach(openRouterKey, new MixOpenRouter({ options, config }));
|
|
461
|
+
return this;
|
|
439
462
|
}
|
|
440
|
-
|
|
441
|
-
|
|
463
|
+
|
|
464
|
+
gpt5(args = {}) {
|
|
465
|
+
return this._attachOpenAIWithOpenRouter('gpt-5', MixOpenAI, args);
|
|
466
|
+
}
|
|
467
|
+
gpt5mini(args = {}) {
|
|
468
|
+
return this._attachOpenAIWithOpenRouter('gpt-5-mini', MixOpenAI, args);
|
|
469
|
+
}
|
|
470
|
+
gpt5nano(args = {}) {
|
|
471
|
+
return this._attachOpenAIWithOpenRouter('gpt-5-nano', MixOpenAI, args);
|
|
442
472
|
}
|
|
443
|
-
|
|
444
|
-
return this.
|
|
473
|
+
gpt51(args = {}) {
|
|
474
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.1', MixOpenAIResponses, args);
|
|
445
475
|
}
|
|
446
|
-
|
|
447
|
-
return this.
|
|
476
|
+
gpt52(args = {}) {
|
|
477
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.2', MixOpenAIResponses, args);
|
|
448
478
|
}
|
|
449
|
-
|
|
450
|
-
return this.
|
|
479
|
+
gpt54(args = {}) {
|
|
480
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.4', MixOpenAIResponses, args);
|
|
451
481
|
}
|
|
452
|
-
|
|
453
|
-
return this.
|
|
482
|
+
gpt54mini(args = {}) {
|
|
483
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.4-mini', MixOpenAIResponses, args);
|
|
454
484
|
}
|
|
455
|
-
|
|
456
|
-
return this.
|
|
485
|
+
gpt54nano(args = {}) {
|
|
486
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.4-nano', MixOpenAIResponses, args);
|
|
457
487
|
}
|
|
458
|
-
|
|
459
|
-
return this.
|
|
460
|
-
}
|
|
461
|
-
gpt54pro({ options = {}, config = {} } = {}) {
|
|
462
|
-
return this.attach('gpt-5.4-pro', new MixOpenAIResponses({ options, config }));
|
|
488
|
+
gpt54pro(args = {}) {
|
|
489
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.4-pro', MixOpenAIResponses, args);
|
|
463
490
|
}
|
|
464
|
-
gpt55(
|
|
465
|
-
return this.
|
|
491
|
+
gpt55(args = {}) {
|
|
492
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.5', MixOpenAIResponses, args);
|
|
466
493
|
}
|
|
467
|
-
gpt55pro(
|
|
468
|
-
return this.
|
|
494
|
+
gpt55pro(args = {}) {
|
|
495
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.5-pro', MixOpenAIResponses, args);
|
|
469
496
|
}
|
|
470
|
-
gpt56sol(
|
|
471
|
-
return this.
|
|
497
|
+
gpt56sol(args = {}) {
|
|
498
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.6-sol', MixOpenAIResponses, args);
|
|
472
499
|
}
|
|
473
|
-
gpt56terra(
|
|
474
|
-
return this.
|
|
500
|
+
gpt56terra(args = {}) {
|
|
501
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.6-terra', MixOpenAIResponses, args);
|
|
475
502
|
}
|
|
476
|
-
gpt56luna(
|
|
477
|
-
return this.
|
|
503
|
+
gpt56luna(args = {}) {
|
|
504
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.6-luna', MixOpenAIResponses, args);
|
|
478
505
|
}
|
|
479
506
|
gptRealtime({ options = {}, config = {} } = {}) {
|
|
480
507
|
return this.attach('gpt-realtime', new MixOpenAIWebSocket({ options, config }));
|
|
@@ -482,11 +509,14 @@ class ModelMix {
|
|
|
482
509
|
gptRealtimeMini({ options = {}, config = {} } = {}) {
|
|
483
510
|
return this.attach('gpt-realtime-mini', new MixOpenAIWebSocket({ options, config }));
|
|
484
511
|
}
|
|
485
|
-
gpt53codex(
|
|
486
|
-
return this.
|
|
487
|
-
}
|
|
488
|
-
gpt53chat(
|
|
489
|
-
return this.
|
|
512
|
+
gpt53codex(args = {}) {
|
|
513
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.3-codex', MixOpenAIResponses, args);
|
|
514
|
+
}
|
|
515
|
+
gpt53chat(args = {}) {
|
|
516
|
+
return this._attachOpenAIWithOpenRouter('gpt-5.3-chat-latest', MixOpenAIResponses, {
|
|
517
|
+
...args,
|
|
518
|
+
openRouterKey: 'openai/gpt-5.3-chat'
|
|
519
|
+
});
|
|
490
520
|
}
|
|
491
521
|
gptOss({ options = {}, config = {}, mix = {} } = {}) {
|
|
492
522
|
mix = { ...this.mix, ...mix };
|
|
@@ -504,6 +534,12 @@ class ModelMix {
|
|
|
504
534
|
fable5(args = {}) {
|
|
505
535
|
return this.fable50(args);
|
|
506
536
|
}
|
|
537
|
+
fable51({ options = {}, config = {}, mix = {} } = {}) {
|
|
538
|
+
mix = { anthropic: true, ...this.mix, ...mix };
|
|
539
|
+
if (mix.anthropic) this.attach('claude-fable-5-1', new MixAnthropic({ options, config }));
|
|
540
|
+
if (mix.openrouter) this.attach('anthropic/claude-fable-5.1', new MixOpenRouter({ options, config }));
|
|
541
|
+
return this;
|
|
542
|
+
}
|
|
507
543
|
opus50({ options = {}, config = {} } = {}) {
|
|
508
544
|
return this.attach('claude-opus-5', new MixAnthropic({ options, config }));
|
|
509
545
|
}
|
|
@@ -585,6 +621,10 @@ class ModelMix {
|
|
|
585
621
|
return this;
|
|
586
622
|
}
|
|
587
623
|
|
|
624
|
+
museSpark12Contributor({ options = {}, config = {} } = {}) {
|
|
625
|
+
return this.attach('meta/muse-spark-1.2-contributor', new MixOpenRouter({ options, config }));
|
|
626
|
+
}
|
|
627
|
+
|
|
588
628
|
qwen35397b({ options = {}, config = {} } = {}) {
|
|
589
629
|
return this.attach('qwen/qwen3.5-397b-a17b', new MixOpenRouter({ options, config }));
|
|
590
630
|
}
|
|
@@ -628,7 +668,7 @@ class ModelMix {
|
|
|
628
668
|
return this.attach('nousresearch/hermes-4-405b', new MixOpenRouter({ options, config }));
|
|
629
669
|
}
|
|
630
670
|
|
|
631
|
-
hermes3({ options = {}, config = {}, mix = {} } = {}) {
|
|
671
|
+
hermes3({ options = {}, config = {}, mix = { openrouter: true } } = {}) {
|
|
632
672
|
mix = { ...this.mix, ...mix };
|
|
633
673
|
if (mix.lambda) this.attach('Hermes-3-Llama-3.1-405B-FP8', new MixLambda({ options, config }));
|
|
634
674
|
if (mix.openrouter) this.attach('nousresearch/hermes-3-llama-3.1-405b:free', new MixOpenRouter({ options, config }));
|
|
@@ -673,7 +713,7 @@ class ModelMix {
|
|
|
673
713
|
}
|
|
674
714
|
|
|
675
715
|
|
|
676
|
-
minimaxM27({ options = {}, config = {}, mix = {
|
|
716
|
+
minimaxM27({ options = {}, config = {}, mix = { minimax: true } } = {}) {
|
|
677
717
|
mix = { ...this.mix, ...mix };
|
|
678
718
|
if (mix.nvidia) this.attach('minimaxai/minimax-m2.7', new MixNVIDIA({ options, config }));
|
|
679
719
|
if (mix.fireworks) this.attach('accounts/fireworks/models/minimax-m2p7', new MixFireworks({ options, config }));
|
|
@@ -786,22 +826,23 @@ class ModelMix {
|
|
|
786
826
|
return this;
|
|
787
827
|
}
|
|
788
828
|
|
|
789
|
-
|
|
829
|
+
_addImageSource(source, { role = "user", cache } = {}) {
|
|
790
830
|
const contentCache = normalizeContentCache(cache);
|
|
791
831
|
this.messages.push({
|
|
792
832
|
role,
|
|
793
833
|
content: [{
|
|
794
834
|
type: "image",
|
|
795
|
-
source
|
|
796
|
-
type: "buffer",
|
|
797
|
-
data: buffer
|
|
798
|
-
},
|
|
835
|
+
source,
|
|
799
836
|
...(contentCache !== undefined && { cache: contentCache })
|
|
800
837
|
}]
|
|
801
838
|
});
|
|
802
839
|
return this;
|
|
803
840
|
}
|
|
804
841
|
|
|
842
|
+
addImageFromBuffer(buffer, { role = "user", cache } = {}) {
|
|
843
|
+
return this._addImageSource({ type: "buffer", data: buffer }, { role, cache });
|
|
844
|
+
}
|
|
845
|
+
|
|
805
846
|
addImage(filePath, { role = "user", cache } = {}) {
|
|
806
847
|
const absolutePath = path.resolve(filePath);
|
|
807
848
|
|
|
@@ -809,19 +850,7 @@ class ModelMix {
|
|
|
809
850
|
throw new Error(`Image file not found: ${filePath}`);
|
|
810
851
|
}
|
|
811
852
|
|
|
812
|
-
|
|
813
|
-
this.messages.push({
|
|
814
|
-
role,
|
|
815
|
-
content: [{
|
|
816
|
-
type: "image",
|
|
817
|
-
source: {
|
|
818
|
-
type: "file",
|
|
819
|
-
data: filePath
|
|
820
|
-
},
|
|
821
|
-
...(contentCache !== undefined && { cache: contentCache })
|
|
822
|
-
}]
|
|
823
|
-
});
|
|
824
|
-
return this;
|
|
853
|
+
return this._addImageSource({ type: "file", data: filePath }, { role, cache });
|
|
825
854
|
}
|
|
826
855
|
|
|
827
856
|
addImageFromUrl(url, { role = "user", cache } = {}) {
|
|
@@ -845,34 +874,28 @@ class ModelMix {
|
|
|
845
874
|
};
|
|
846
875
|
}
|
|
847
876
|
|
|
848
|
-
|
|
849
|
-
this.messages.push({
|
|
850
|
-
role,
|
|
851
|
-
content: [{
|
|
852
|
-
type: "image",
|
|
853
|
-
source,
|
|
854
|
-
...(contentCache !== undefined && { cache: contentCache })
|
|
855
|
-
}]
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
return this;
|
|
877
|
+
return this._addImageSource(source, { role, cache });
|
|
859
878
|
}
|
|
860
879
|
|
|
861
|
-
async processImages() {
|
|
862
|
-
|
|
863
|
-
|
|
880
|
+
async processImages(signal) {
|
|
881
|
+
assertAbortSignal(signal);
|
|
882
|
+
const preparedContent = [];
|
|
883
|
+
for (const message of this.messages) {
|
|
864
884
|
if (!Array.isArray(message.content)) continue;
|
|
885
|
+
const nextContent = [];
|
|
865
886
|
|
|
866
|
-
for (
|
|
867
|
-
|
|
868
|
-
|
|
887
|
+
for (const content of message.content) {
|
|
888
|
+
if (content.type !== 'image' || content.source.type === 'base64') {
|
|
889
|
+
nextContent.push(content);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
869
892
|
|
|
870
893
|
try {
|
|
871
894
|
let buffer, mimeType;
|
|
872
895
|
|
|
873
896
|
switch (content.source.type) {
|
|
874
897
|
case 'url':
|
|
875
|
-
const response = await fetchBinaryResponse(content.source.data);
|
|
898
|
+
const response = await fetchBinaryResponse(content.source.data, { signal });
|
|
876
899
|
buffer = response.data;
|
|
877
900
|
mimeType = response.headers['content-type'];
|
|
878
901
|
break;
|
|
@@ -886,6 +909,8 @@ class ModelMix {
|
|
|
886
909
|
break;
|
|
887
910
|
}
|
|
888
911
|
|
|
912
|
+
throwIfAborted(signal);
|
|
913
|
+
|
|
889
914
|
// Detect mimeType if not provided
|
|
890
915
|
if (!mimeType) {
|
|
891
916
|
if (typeof detectFileTypeFromBuffer !== 'function') {
|
|
@@ -898,32 +923,33 @@ class ModelMix {
|
|
|
898
923
|
mimeType = detectedType.mime;
|
|
899
924
|
}
|
|
900
925
|
|
|
901
|
-
|
|
902
|
-
message.content[j] = {
|
|
926
|
+
nextContent.push({
|
|
903
927
|
...content,
|
|
904
928
|
source: {
|
|
905
929
|
type: "base64",
|
|
906
930
|
media_type: mimeType,
|
|
907
931
|
data: buffer.toString('base64')
|
|
908
932
|
}
|
|
909
|
-
};
|
|
933
|
+
});
|
|
910
934
|
|
|
911
935
|
} catch (error) {
|
|
936
|
+
throwIfAborted(signal);
|
|
912
937
|
console.error(`Error processing image:`, error);
|
|
913
|
-
// Remove failed image from content
|
|
914
|
-
message.content.splice(j, 1);
|
|
915
|
-
j--;
|
|
916
938
|
}
|
|
917
939
|
}
|
|
940
|
+
preparedContent.push({ message, content: nextContent });
|
|
918
941
|
}
|
|
942
|
+
throwIfAborted(signal);
|
|
943
|
+
for (const prepared of preparedContent) prepared.message.content = prepared.content;
|
|
919
944
|
}
|
|
920
945
|
|
|
921
|
-
async message() {
|
|
922
|
-
let raw = await this.execute({ options: { stream: false }, outputMode: 'message' });
|
|
946
|
+
async message(signal) {
|
|
947
|
+
let raw = await this.execute({ options: { stream: false }, outputMode: 'message', signal });
|
|
923
948
|
return raw.message;
|
|
924
949
|
}
|
|
925
950
|
|
|
926
|
-
async json(schemaExample = null, schemaDescription = {}, { type = 'json_object', addExample = false, addSchema = true, addNote = false } = {}) {
|
|
951
|
+
async json(schemaExample = null, schemaDescription = {}, { type = 'json_object', addExample = false, addSchema = true, addNote = false } = {}, signal) {
|
|
952
|
+
assertAbortSignal(signal);
|
|
927
953
|
|
|
928
954
|
let isArrayWrap = false;
|
|
929
955
|
if (Array.isArray(schemaExample)) {
|
|
@@ -955,7 +981,7 @@ class ModelMix {
|
|
|
955
981
|
systemSuffix += "\n\nOutput JSON Escape: double quotes, backslashes, and control characters inside JSON strings.\nEnsure the output contains no comments.";
|
|
956
982
|
}
|
|
957
983
|
}
|
|
958
|
-
const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json' });
|
|
984
|
+
const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json', signal });
|
|
959
985
|
const parsed = JSON.parse(this._extractBlock(message));
|
|
960
986
|
return isArrayWrap ? parsed.out : parsed;
|
|
961
987
|
}
|
|
@@ -965,25 +991,28 @@ class ModelMix {
|
|
|
965
991
|
return block ? block[1].trim() : response.trim();
|
|
966
992
|
}
|
|
967
993
|
|
|
968
|
-
async block({ addSystemExtra = true } = {}) {
|
|
994
|
+
async block({ addSystemExtra = true } = {}, signal) {
|
|
995
|
+
assertAbortSignal(signal);
|
|
969
996
|
const systemSuffix = addSystemExtra
|
|
970
997
|
? "\nReturn the result of the task between triple backtick block code tags ```"
|
|
971
998
|
: '';
|
|
972
999
|
const { message } = await this.execute({
|
|
973
1000
|
options: { stream: false },
|
|
974
1001
|
systemSuffix,
|
|
975
|
-
outputMode: 'block'
|
|
1002
|
+
outputMode: 'block',
|
|
1003
|
+
signal
|
|
976
1004
|
});
|
|
977
1005
|
return this._extractBlock(message);
|
|
978
1006
|
}
|
|
979
1007
|
|
|
980
|
-
async raw() {
|
|
981
|
-
return this.execute({ options: { stream: false }, outputMode: 'raw' });
|
|
1008
|
+
async raw(signal) {
|
|
1009
|
+
return this.execute({ options: { stream: false }, outputMode: 'raw', signal });
|
|
982
1010
|
}
|
|
983
1011
|
|
|
984
|
-
async stream(callback) {
|
|
1012
|
+
async stream(callback, signal) {
|
|
1013
|
+
assertAbortSignal(signal);
|
|
985
1014
|
this.streamCallback = callback;
|
|
986
|
-
return this.execute({ options: { stream: true }, outputMode: 'stream' });
|
|
1015
|
+
return this.execute({ options: { stream: true }, outputMode: 'stream', signal });
|
|
987
1016
|
}
|
|
988
1017
|
|
|
989
1018
|
assignKeyFromFile(key, filePath) {
|
|
@@ -1096,10 +1125,6 @@ class ModelMix {
|
|
|
1096
1125
|
const shouldNotGroup = ModelMix.hasToolInteraction(currentMessage);
|
|
1097
1126
|
|
|
1098
1127
|
if (index === 0 || currentMessage.role !== messages[index - 1].role || shouldNotGroup) {
|
|
1099
|
-
// acc.push({
|
|
1100
|
-
// role: currentMessage.role,
|
|
1101
|
-
// content: currentMessage.content
|
|
1102
|
-
// });
|
|
1103
1128
|
acc.push(currentMessage);
|
|
1104
1129
|
} else {
|
|
1105
1130
|
acc[acc.length - 1].content = acc[acc.length - 1].content.concat(currentMessage.content);
|
|
@@ -1144,8 +1169,8 @@ class ModelMix {
|
|
|
1144
1169
|
}
|
|
1145
1170
|
}
|
|
1146
1171
|
|
|
1147
|
-
async prepareMessages(renderContext = createTemplateRenderContext(() => this._choiceRandom())) {
|
|
1148
|
-
await this.processImages();
|
|
1172
|
+
async prepareMessages(renderContext = createTemplateRenderContext(() => this._choiceRandom()), signal) {
|
|
1173
|
+
await this.processImages(signal);
|
|
1149
1174
|
|
|
1150
1175
|
let messages = this.messages;
|
|
1151
1176
|
|
|
@@ -1237,13 +1262,13 @@ class ModelMix {
|
|
|
1237
1262
|
async _executePlugins({
|
|
1238
1263
|
config,
|
|
1239
1264
|
options,
|
|
1265
|
+
signal,
|
|
1240
1266
|
systemSuffix,
|
|
1241
1267
|
outputMode,
|
|
1242
1268
|
templateContext,
|
|
1243
|
-
executionMetadata
|
|
1244
|
-
isRootExecution
|
|
1269
|
+
executionMetadata
|
|
1245
1270
|
}) {
|
|
1246
|
-
const preparedMessages = await this.prepareMessages(templateContext);
|
|
1271
|
+
const preparedMessages = await this.prepareMessages(templateContext, signal);
|
|
1247
1272
|
this._requirePreparedMessages(preparedMessages);
|
|
1248
1273
|
|
|
1249
1274
|
const request = {
|
|
@@ -1266,6 +1291,7 @@ class ModelMix {
|
|
|
1266
1291
|
return this.execute({
|
|
1267
1292
|
config,
|
|
1268
1293
|
options,
|
|
1294
|
+
signal,
|
|
1269
1295
|
systemSuffix,
|
|
1270
1296
|
outputMode,
|
|
1271
1297
|
_templateContext: templateContext,
|
|
@@ -1275,6 +1301,7 @@ class ModelMix {
|
|
|
1275
1301
|
});
|
|
1276
1302
|
}
|
|
1277
1303
|
|
|
1304
|
+
throwIfAborted(signal);
|
|
1278
1305
|
const plugin = this.plugins[index];
|
|
1279
1306
|
let nextCalled = false;
|
|
1280
1307
|
const next = () => {
|
|
@@ -1287,9 +1314,11 @@ class ModelMix {
|
|
|
1287
1314
|
const context = {
|
|
1288
1315
|
request,
|
|
1289
1316
|
execution: Object.freeze({ ...metadata }),
|
|
1290
|
-
|
|
1317
|
+
signal,
|
|
1318
|
+
invoke: input => this._invokeChild(input, metadata, signal)
|
|
1291
1319
|
};
|
|
1292
1320
|
const result = await plugin.execute(context, next);
|
|
1321
|
+
throwIfAborted(signal);
|
|
1293
1322
|
return validatePluginResult(result, plugin.name);
|
|
1294
1323
|
};
|
|
1295
1324
|
|
|
@@ -1302,7 +1331,6 @@ class ModelMix {
|
|
|
1302
1331
|
this._addText(result.message, { role: 'assistant' });
|
|
1303
1332
|
}
|
|
1304
1333
|
}
|
|
1305
|
-
if (isRootExecution) this._commitTemplateRenderContext(templateContext);
|
|
1306
1334
|
return result;
|
|
1307
1335
|
}
|
|
1308
1336
|
|
|
@@ -1349,6 +1377,8 @@ class ModelMix {
|
|
|
1349
1377
|
currentConfig.system = pluginRequest
|
|
1350
1378
|
? pluginRequest.system
|
|
1351
1379
|
: this._renderSystem(config, provider.config, systemSuffix, templateContext);
|
|
1380
|
+
assertNoStoredSignal(currentOptions, 'options');
|
|
1381
|
+
assertNoStoredSignal(currentConfig, 'config');
|
|
1352
1382
|
|
|
1353
1383
|
const resolvedModelKey = resolveGrok420ModelKey(
|
|
1354
1384
|
currentModel.key,
|
|
@@ -1385,7 +1415,7 @@ class ModelMix {
|
|
|
1385
1415
|
}
|
|
1386
1416
|
}
|
|
1387
1417
|
|
|
1388
|
-
async _invokeProviderWithRetry(provider, currentOptions, currentConfig, resolvedModelKey) {
|
|
1418
|
+
async _invokeProviderWithRetry(provider, currentOptions, currentConfig, resolvedModelKey, signal) {
|
|
1389
1419
|
if (currentOptions.stream && this.streamCallback) {
|
|
1390
1420
|
provider.streamCallback = this.streamCallback;
|
|
1391
1421
|
}
|
|
@@ -1404,9 +1434,12 @@ class ModelMix {
|
|
|
1404
1434
|
while (true) {
|
|
1405
1435
|
const startTime = Date.now();
|
|
1406
1436
|
try {
|
|
1407
|
-
|
|
1437
|
+
throwIfAborted(signal);
|
|
1438
|
+
const result = await provider.create({ options: currentOptions, config: currentConfig, signal });
|
|
1439
|
+
throwIfAborted(signal);
|
|
1408
1440
|
return { result, elapsedMs: Date.now() - startTime };
|
|
1409
1441
|
} catch (error) {
|
|
1442
|
+
throwIfAborted(signal);
|
|
1410
1443
|
const statusCode = getErrorStatusCode(error);
|
|
1411
1444
|
if (attempt >= retries || !retryableStatusCodes.has(statusCode)) throw error;
|
|
1412
1445
|
|
|
@@ -1414,7 +1447,7 @@ class ModelMix {
|
|
|
1414
1447
|
console.log(`↺ Retrying [${resolvedModelKey}] due to status ${statusCode} (${attempt + 2}/${retries + 1})`);
|
|
1415
1448
|
}
|
|
1416
1449
|
const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
1417
|
-
await
|
|
1450
|
+
await sleepWithSignal(delay, signal);
|
|
1418
1451
|
attempt += 1;
|
|
1419
1452
|
}
|
|
1420
1453
|
}
|
|
@@ -1438,9 +1471,10 @@ class ModelMix {
|
|
|
1438
1471
|
}
|
|
1439
1472
|
|
|
1440
1473
|
async _continueToolCalls(result, pluginRequest, execution) {
|
|
1474
|
+
const originalMessages = this.messages;
|
|
1441
1475
|
const toolMessages = pluginRequest
|
|
1442
1476
|
? clonePluginValue(pluginRequest.messages)
|
|
1443
|
-
: this.messages;
|
|
1477
|
+
: clonePluginValue(this.messages);
|
|
1444
1478
|
if (result.assistantMessage) {
|
|
1445
1479
|
toolMessages.push(result.assistantMessage);
|
|
1446
1480
|
} else if (result.message) {
|
|
@@ -1464,7 +1498,7 @@ class ModelMix {
|
|
|
1464
1498
|
if (!result.assistantMessage) {
|
|
1465
1499
|
toolMessages.push({ role: 'assistant', content: null, tool_calls: result.toolCalls });
|
|
1466
1500
|
}
|
|
1467
|
-
const toolResults = await this.processToolCalls(result.toolCalls);
|
|
1501
|
+
const toolResults = await this.processToolCalls(result.toolCalls, execution.signal);
|
|
1468
1502
|
for (const toolResult of toolResults) {
|
|
1469
1503
|
toolMessages.push({
|
|
1470
1504
|
role: 'tool',
|
|
@@ -1475,12 +1509,17 @@ class ModelMix {
|
|
|
1475
1509
|
}
|
|
1476
1510
|
this.messages = toolMessages;
|
|
1477
1511
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1512
|
+
try {
|
|
1513
|
+
return await this.execute({
|
|
1514
|
+
...execution,
|
|
1515
|
+
_pluginRequest: pluginRequest
|
|
1516
|
+
? { ...pluginRequest, messages: toolMessages }
|
|
1517
|
+
: null
|
|
1518
|
+
});
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
if (execution.signal?.aborted) this.messages = originalMessages;
|
|
1521
|
+
throw error;
|
|
1522
|
+
}
|
|
1484
1523
|
}
|
|
1485
1524
|
|
|
1486
1525
|
_logProviderSuccess(result, currentConfig) {
|
|
@@ -1555,6 +1594,7 @@ class ModelMix {
|
|
|
1555
1594
|
async _executeProviderChain({
|
|
1556
1595
|
config,
|
|
1557
1596
|
options,
|
|
1597
|
+
signal,
|
|
1558
1598
|
systemSuffix,
|
|
1559
1599
|
outputMode,
|
|
1560
1600
|
templateContext,
|
|
@@ -1564,7 +1604,7 @@ class ModelMix {
|
|
|
1564
1604
|
}) {
|
|
1565
1605
|
const preparedMessages = pluginRequest
|
|
1566
1606
|
? pluginRequest.messages
|
|
1567
|
-
: await this.prepareMessages(templateContext);
|
|
1607
|
+
: await this.prepareMessages(templateContext, signal);
|
|
1568
1608
|
this._requirePreparedMessages(preparedMessages);
|
|
1569
1609
|
|
|
1570
1610
|
const finalConfig = pluginRequest ? pluginRequest.config : this._mergeRequestConfig(config);
|
|
@@ -1598,7 +1638,8 @@ class ModelMix {
|
|
|
1598
1638
|
providerAttempt.provider,
|
|
1599
1639
|
providerAttempt.currentOptions,
|
|
1600
1640
|
providerAttempt.currentConfig,
|
|
1601
|
-
providerAttempt.resolvedModelKey
|
|
1641
|
+
providerAttempt.resolvedModelKey,
|
|
1642
|
+
signal
|
|
1602
1643
|
);
|
|
1603
1644
|
this._enrichResultTokens(result, providerAttempt.resolvedModelKey, elapsedMs);
|
|
1604
1645
|
|
|
@@ -1606,6 +1647,7 @@ class ModelMix {
|
|
|
1606
1647
|
return this._continueToolCalls(result, pluginRequest, {
|
|
1607
1648
|
options,
|
|
1608
1649
|
config,
|
|
1650
|
+
signal,
|
|
1609
1651
|
systemSuffix,
|
|
1610
1652
|
outputMode,
|
|
1611
1653
|
_templateContext: templateContext,
|
|
@@ -1618,6 +1660,7 @@ class ModelMix {
|
|
|
1618
1660
|
this._recordProviderResult(result);
|
|
1619
1661
|
return result;
|
|
1620
1662
|
} catch (error) {
|
|
1663
|
+
throwIfAborted(signal);
|
|
1621
1664
|
lastError = error;
|
|
1622
1665
|
this._logProviderFailure(error, currentModel.key, attempt, modelsToTry);
|
|
1623
1666
|
}
|
|
@@ -1630,6 +1673,7 @@ class ModelMix {
|
|
|
1630
1673
|
async execute({
|
|
1631
1674
|
config = {},
|
|
1632
1675
|
options = {},
|
|
1676
|
+
signal,
|
|
1633
1677
|
systemSuffix = '',
|
|
1634
1678
|
outputMode = 'raw',
|
|
1635
1679
|
_templateContext = null,
|
|
@@ -1637,43 +1681,56 @@ class ModelMix {
|
|
|
1637
1681
|
_executionMetadata = null,
|
|
1638
1682
|
_pluginsApplied = false
|
|
1639
1683
|
} = {}) {
|
|
1684
|
+
assertAbortSignal(signal);
|
|
1685
|
+
assertNoStoredSignal(this.config, 'config');
|
|
1686
|
+
assertNoStoredSignal(this.options, 'options');
|
|
1687
|
+
assertNoStoredSignal(config, 'config');
|
|
1688
|
+
assertNoStoredSignal(options, 'options');
|
|
1689
|
+
for (const model of this.models) {
|
|
1690
|
+
assertNoStoredSignal(model.provider?.config, 'provider.config');
|
|
1691
|
+
assertNoStoredSignal(model.provider?.options, 'provider.options');
|
|
1692
|
+
}
|
|
1640
1693
|
const isRootExecution = _templateContext === null;
|
|
1641
1694
|
const templateContext = _templateContext || createTemplateRenderContext(() => this._choiceRandom());
|
|
1695
|
+
let execution;
|
|
1642
1696
|
|
|
1643
1697
|
if (!_pluginsApplied && this.plugins.length > 0) {
|
|
1644
|
-
|
|
1698
|
+
execution = this._executePlugins({
|
|
1645
1699
|
config,
|
|
1646
1700
|
options,
|
|
1701
|
+
signal,
|
|
1647
1702
|
systemSuffix,
|
|
1648
1703
|
outputMode,
|
|
1649
1704
|
templateContext,
|
|
1650
|
-
executionMetadata: _executionMetadata
|
|
1651
|
-
|
|
1705
|
+
executionMetadata: _executionMetadata
|
|
1706
|
+
});
|
|
1707
|
+
} else {
|
|
1708
|
+
if (!this.models || this.models.length === 0) {
|
|
1709
|
+
throw new Error('No models specified. Use methods like .gpt5(), .sonnet46() first.');
|
|
1710
|
+
}
|
|
1711
|
+
execution = this.limiter.schedule(() => {
|
|
1712
|
+
throwIfAborted(signal);
|
|
1713
|
+
return this._executeProviderChain({
|
|
1714
|
+
config,
|
|
1715
|
+
options,
|
|
1716
|
+
signal,
|
|
1717
|
+
systemSuffix,
|
|
1718
|
+
outputMode,
|
|
1719
|
+
templateContext,
|
|
1720
|
+
pluginRequest: _pluginRequest,
|
|
1721
|
+
executionMetadata: _executionMetadata,
|
|
1722
|
+
pluginsApplied: _pluginsApplied
|
|
1723
|
+
});
|
|
1652
1724
|
});
|
|
1653
1725
|
}
|
|
1654
1726
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
const execution = this.limiter.schedule(() => this._executeProviderChain({
|
|
1660
|
-
config,
|
|
1661
|
-
options,
|
|
1662
|
-
systemSuffix,
|
|
1663
|
-
outputMode,
|
|
1664
|
-
templateContext,
|
|
1665
|
-
pluginRequest: _pluginRequest,
|
|
1666
|
-
executionMetadata: _executionMetadata,
|
|
1667
|
-
pluginsApplied: _pluginsApplied
|
|
1668
|
-
}));
|
|
1669
|
-
|
|
1670
|
-
if (!isRootExecution) return execution;
|
|
1671
|
-
|
|
1672
|
-
const result = await execution;
|
|
1673
|
-
this._commitTemplateRenderContext(templateContext);
|
|
1727
|
+
const result = await raceWithSignal(execution, signal);
|
|
1728
|
+
throwIfAborted(signal);
|
|
1729
|
+
if (isRootExecution) this._commitTemplateRenderContext(templateContext);
|
|
1674
1730
|
return result;
|
|
1675
1731
|
}
|
|
1676
|
-
async processToolCalls(toolCalls) {
|
|
1732
|
+
async processToolCalls(toolCalls, signal) {
|
|
1733
|
+
assertAbortSignal(signal);
|
|
1677
1734
|
const result = []
|
|
1678
1735
|
|
|
1679
1736
|
for (const toolCall of toolCalls) {
|
|
@@ -1706,7 +1763,8 @@ class ModelMix {
|
|
|
1706
1763
|
|
|
1707
1764
|
// Verificar si es una herramienta local registrada
|
|
1708
1765
|
if (this.mcpToolsManager.hasTool(toolName)) {
|
|
1709
|
-
const response = await this.mcpToolsManager.executeTool(toolName, toolArgs);
|
|
1766
|
+
const response = await this.mcpToolsManager.executeTool(toolName, toolArgs, signal);
|
|
1767
|
+
throwIfAborted(signal);
|
|
1710
1768
|
result.push({
|
|
1711
1769
|
name: toolName,
|
|
1712
1770
|
tool_call_id: toolId,
|
|
@@ -1722,7 +1780,8 @@ class ModelMix {
|
|
|
1722
1780
|
const response = await client.callTool({
|
|
1723
1781
|
name: toolName,
|
|
1724
1782
|
arguments: toolArgs
|
|
1725
|
-
});
|
|
1783
|
+
}, undefined, signal ? { signal } : undefined);
|
|
1784
|
+
throwIfAborted(signal);
|
|
1726
1785
|
|
|
1727
1786
|
result.push({
|
|
1728
1787
|
name: toolName,
|
|
@@ -1731,6 +1790,7 @@ class ModelMix {
|
|
|
1731
1790
|
});
|
|
1732
1791
|
}
|
|
1733
1792
|
} catch (error) {
|
|
1793
|
+
throwIfAborted(signal);
|
|
1734
1794
|
console.error(`Error processing tool call ${toolName}:`, error);
|
|
1735
1795
|
result.push({
|
|
1736
1796
|
name: toolName || 'unknown',
|
|
@@ -1865,4 +1925,4 @@ class ModelMix {
|
|
|
1865
1925
|
log
|
|
1866
1926
|
}));
|
|
1867
1927
|
|
|
1868
|
-
module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
|
|
1928
|
+
module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLambda, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
|