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.
Files changed (45) hide show
  1. package/README.md +42 -10
  2. package/demo/free.js +1 -3
  3. package/demo/short.js +15 -13
  4. package/effort.js +13 -7
  5. package/http-client.js +6 -6
  6. package/index.d.ts +13 -6
  7. package/index.js +204 -144
  8. package/lib/abort-signal.js +57 -0
  9. package/lib/model-chain.js +2 -2
  10. package/lib/provider-api-key.js +9 -0
  11. package/lib/providers/anthropic.js +24 -22
  12. package/lib/providers/base.js +21 -28
  13. package/lib/providers/google.js +12 -8
  14. package/lib/providers/openai-compatible.js +19 -48
  15. package/lib/providers/openai-options.js +16 -0
  16. package/lib/providers/openai.js +48 -47
  17. package/lib/providers.js +5 -2
  18. package/lib/token-usage.js +19 -4
  19. package/mcp-tools.js +5 -2
  20. package/package.json +3 -2
  21. package/plugins/rlm/index.d.ts +1 -0
  22. package/plugins/rlm/lib/budget.js +2 -7
  23. package/plugins/rlm/lib/isolated-vm-sandbox.js +8 -1
  24. package/plugins/rlm/lib/planner-prompt.js +1 -7
  25. package/plugins/rlm/lib/plugin.js +24 -11
  26. package/plugins/rlm/lib/validation.js +14 -0
  27. package/plugins/rlm/lib/variable-descriptors.js +1 -5
  28. package/plugins/rlm/lib/worker-catalog.js +1 -5
  29. package/plugins/rlm/test/isolated-vm-sandbox.test.js +23 -0
  30. package/pnpm-workspace.yaml +10 -5
  31. package/schema.js +0 -28
  32. package/skills/modelmix/SKILL.md +34 -12
  33. package/test/abort.test.js +517 -0
  34. package/test/anthropic.test.js +77 -1
  35. package/test/bottleneck.test.js +2 -2
  36. package/test/effort.test.js +24 -1
  37. package/test/fallback.test.js +101 -1
  38. package/test/glm.test.js +2 -6
  39. package/test/kimi.test.js +2 -6
  40. package/test/live.test.js +10 -5
  41. package/test/muse.test.js +24 -7
  42. package/test/provider-expansion.test.js +17 -0
  43. package/test/public-api.test.js +2 -2
  44. package/test/qwen.test.js +2 -3
  45. 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: true, cerebras: true, groq: true, together: false, lambda: false };
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
- gpt5({ options = {}, config = {} } = {}) {
438
- return this.attach('gpt-5', new MixOpenAI({ options, config }));
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
- gpt5mini({ options = {}, config = {} } = {}) {
441
- return this.attach('gpt-5-mini', new MixOpenAI({ options, config }));
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
- gpt5nano({ options = {}, config = {} } = {}) {
444
- return this.attach('gpt-5-nano', new MixOpenAI({ options, config }));
473
+ gpt51(args = {}) {
474
+ return this._attachOpenAIWithOpenRouter('gpt-5.1', MixOpenAIResponses, args);
445
475
  }
446
- gpt51({ options = {}, config = {} } = {}) {
447
- return this.attach('gpt-5.1', new MixOpenAIResponses({ options, config }));
476
+ gpt52(args = {}) {
477
+ return this._attachOpenAIWithOpenRouter('gpt-5.2', MixOpenAIResponses, args);
448
478
  }
449
- gpt52({ options = {}, config = {} } = {}) {
450
- return this.attach('gpt-5.2', new MixOpenAIResponses({ options, config }));
479
+ gpt54(args = {}) {
480
+ return this._attachOpenAIWithOpenRouter('gpt-5.4', MixOpenAIResponses, args);
451
481
  }
452
- gpt54({ options = {}, config = {} } = {}) {
453
- return this.attach('gpt-5.4', new MixOpenAIResponses({ options, config }));
482
+ gpt54mini(args = {}) {
483
+ return this._attachOpenAIWithOpenRouter('gpt-5.4-mini', MixOpenAIResponses, args);
454
484
  }
455
- gpt54mini({ options = {}, config = {} } = {}) {
456
- return this.attach('gpt-5.4-mini', new MixOpenAIResponses({ options, config }));
485
+ gpt54nano(args = {}) {
486
+ return this._attachOpenAIWithOpenRouter('gpt-5.4-nano', MixOpenAIResponses, args);
457
487
  }
458
- gpt54nano({ options = {}, config = {} } = {}) {
459
- return this.attach('gpt-5.4-nano', new MixOpenAIResponses({ options, config }));
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({ options = {}, config = {} } = {}) {
465
- return this.attach('gpt-5.5', new MixOpenAIResponses({ options, config }));
491
+ gpt55(args = {}) {
492
+ return this._attachOpenAIWithOpenRouter('gpt-5.5', MixOpenAIResponses, args);
466
493
  }
467
- gpt55pro({ options = {}, config = {} } = {}) {
468
- return this.attach('gpt-5.5-pro', new MixOpenAIResponses({ options, config }));
494
+ gpt55pro(args = {}) {
495
+ return this._attachOpenAIWithOpenRouter('gpt-5.5-pro', MixOpenAIResponses, args);
469
496
  }
470
- gpt56sol({ options = {}, config = {} } = {}) {
471
- return this.attach('gpt-5.6-sol', new MixOpenAIResponses({ options, config }));
497
+ gpt56sol(args = {}) {
498
+ return this._attachOpenAIWithOpenRouter('gpt-5.6-sol', MixOpenAIResponses, args);
472
499
  }
473
- gpt56terra({ options = {}, config = {} } = {}) {
474
- return this.attach('gpt-5.6-terra', new MixOpenAIResponses({ options, config }));
500
+ gpt56terra(args = {}) {
501
+ return this._attachOpenAIWithOpenRouter('gpt-5.6-terra', MixOpenAIResponses, args);
475
502
  }
476
- gpt56luna({ options = {}, config = {} } = {}) {
477
- return this.attach('gpt-5.6-luna', new MixOpenAIResponses({ options, config }));
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({ options = {}, config = {} } = {}) {
486
- return this.attach('gpt-5.3-codex', new MixOpenAIResponses({ options, config }));
487
- }
488
- gpt53chat({ options = {}, config = {} } = {}) {
489
- return this.attach('gpt-5.3-chat-latest', new MixOpenAIResponses({ options, config }));
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 = { openrouter: true, minimax: true } } = {}) {
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
- addImageFromBuffer(buffer, { role = "user", cache } = {}) {
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
- const contentCache = normalizeContentCache(cache);
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
- const contentCache = normalizeContentCache(cache);
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
- for (let i = 0; i < this.messages.length; i++) {
863
- const message = this.messages[i];
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 (let j = 0; j < message.content.length; j++) {
867
- const content = message.content[j];
868
- if (content.type !== 'image' || content.source.type === 'base64') continue;
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
- // Update the content with processed image
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
- invoke: input => this._invokeChild(input, metadata)
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
- const result = await provider.create({ options: currentOptions, config: currentConfig });
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 sleep(delay);
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
- return this.execute({
1479
- ...execution,
1480
- _pluginRequest: pluginRequest
1481
- ? { ...pluginRequest, messages: toolMessages }
1482
- : null
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
- return this._executePlugins({
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
- isRootExecution
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
- if (!this.models || this.models.length === 0) {
1656
- throw new Error('No models specified. Use methods like .gpt5(), .sonnet46() first.');
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 };