modelmix 5.1.12 → 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 CHANGED
@@ -95,6 +95,26 @@ const model = await ModelMix.new(setup)
95
95
  console.log(await model.message());
96
96
  ```
97
97
 
98
+ **Cancel one execution with AbortSignal**
99
+
100
+ Pass the signal directly to the terminal method. ModelMix propagates it through
101
+ queued work, retries, fallbacks, provider transports, plugins, child executions,
102
+ and tool calls:
103
+
104
+ ```javascript
105
+ const controller = new AbortController();
106
+ const response = model.message(controller.signal);
107
+
108
+ controller.abort(new Error('Request cancelled'));
109
+ await response;
110
+ ```
111
+
112
+ Use `raw(signal)`, `stream(callback, signal)`, `block(options, signal)`, or
113
+ `json(example, description, jsonOptions, signal)` for the other output modes.
114
+ `execute()` accepts it as the top-level `signal` field. Never put a signal in
115
+ ModelMix or provider `config`/`options`; those locations are rejected because
116
+ they are reusable and may become provider payload fields.
117
+
98
118
  The same ordered chain can be attached by passing model shortcuts directly to
99
119
  `chain()`. Add `@effort` to override unified effort for one model; entries
100
120
  without it inherit the chain effort, or use the provider default when the chain
@@ -939,7 +959,7 @@ const model = ModelMix.new()
939
959
  .addText('Summarize this request.');
940
960
  ```
941
961
 
942
- A plugin may edit `context.request`, call `next()`, or return a complete ModelMix result itself. It can also create history-free child executions with `context.invoke()` and choose plugin inheritance:
962
+ A plugin may edit `context.request`, call `next()`, or return a complete ModelMix result itself. The current execution signal is available as `context.signal`, and child executions created with `context.invoke()` inherit it automatically. Local tool callbacks receive the same signal as their second argument. A plugin can also choose plugin inheritance:
943
963
 
944
964
  ```javascript
945
965
  const child = await context.invoke({
@@ -1050,8 +1070,8 @@ new ModelMix(args = { options: {}, config: {} })
1050
1070
  - `assign(keyValues)`: Assigns EJS data for messages and system prompts.
1051
1071
  - `assignKey(key, value)`: Assigns one EJS data value.
1052
1072
  - `assignKeyFromFile(key, filePath)`: Renders an EJS file through `include` and assigns its output to one key.
1053
- - `message()`: Sends the message and returns the response.
1054
- - `raw()`: Sends the message and returns the complete response data including:
1073
+ - `message(signal?)`: Sends the message and returns the response.
1074
+ - `raw(signal?)`: Sends the message and returns the complete response data including:
1055
1075
  - `message`: The text response from the model
1056
1076
  - `think`: Reasoning/thinking content (if available)
1057
1077
  - `toolCalls`: Array of tool calls made by the model (if any)
@@ -1069,8 +1089,8 @@ new ModelMix(args = { options: {}, config: {} })
1069
1089
 
1070
1090
  if (profile.flagged) throw new Error('Profile rejected by moderation');
1071
1091
  ```
1072
- - `stream(callback)`: Sends the message and streams the response, invoking the callback with each streamed part.
1073
- - `json(schemaExample, descriptions = {}, options = {})`: Forces the model to return a response in a specific JSON format.
1092
+ - `stream(callback, signal?)`: Sends the message and streams the response, invoking the callback with each streamed part.
1093
+ - `json(schemaExample, descriptions = {}, options = {}, signal?)`: Forces the model to return a response in a specific JSON format.
1074
1094
  - `schemaExample`: Example of the JSON structure to be returned. Top-level arrays are auto-wrapped for better LLM compatibility.
1075
1095
  - `descriptions`: Descriptions for each field — can be strings or descriptor objects with `{ description, required, enum, default }`.
1076
1096
  - `options`: `{ addSchema: true, addExample: false, addNote: false }`
package/http-client.js CHANGED
@@ -45,8 +45,8 @@ async function buildHttpError(url, response) {
45
45
  return error;
46
46
  }
47
47
 
48
- async function fetchJsonResponse(url, { method = 'POST', headers = {}, body } = {}) {
49
- const response = await fetch(url, { method, headers, body });
48
+ async function fetchJsonResponse(url, { method = 'POST', headers = {}, body, signal } = {}) {
49
+ const response = await fetch(url, { method, headers, body, signal });
50
50
  if (!response.ok) {
51
51
  throw await buildHttpError(url, response);
52
52
  }
@@ -58,8 +58,8 @@ async function fetchJsonResponse(url, { method = 'POST', headers = {}, body } =
58
58
  };
59
59
  }
60
60
 
61
- async function fetchBinaryResponse(url, { method = 'GET', headers = {}, body } = {}) {
62
- const response = await fetch(url, { method, headers, body });
61
+ async function fetchBinaryResponse(url, { method = 'GET', headers = {}, body, signal } = {}) {
62
+ const response = await fetch(url, { method, headers, body, signal });
63
63
  if (!response.ok) {
64
64
  throw await buildHttpError(url, response);
65
65
  }
@@ -71,8 +71,8 @@ async function fetchBinaryResponse(url, { method = 'GET', headers = {}, body } =
71
71
  };
72
72
  }
73
73
 
74
- async function fetchStreamResponse(url, { method = 'POST', headers = {}, body } = {}) {
75
- const response = await fetch(url, { method, headers, body });
74
+ async function fetchStreamResponse(url, { method = 'POST', headers = {}, body, signal } = {}) {
75
+ const response = await fetch(url, { method, headers, body, signal });
76
76
  if (!response.ok) {
77
77
  throw await buildHttpError(url, response);
78
78
  }
package/index.d.ts CHANGED
@@ -259,6 +259,7 @@ export interface PluginExecutionContext {
259
259
  outputMode: ModelMixOutputMode;
260
260
  };
261
261
  execution: Readonly<PluginExecutionMetadata>;
262
+ signal?: AbortSignal;
262
263
  invoke(input: ChildInvocation): Promise<ModelMixResult>;
263
264
  }
264
265
 
@@ -312,7 +313,8 @@ export interface ToolDefinition {
312
313
  }
313
314
 
314
315
  export type ToolCallback = (
315
- args: Record<string, unknown>
316
+ args: Record<string, unknown>,
317
+ signal?: AbortSignal
316
318
  ) => unknown | Promise<unknown>;
317
319
 
318
320
  export interface ToolWithCallback {
@@ -343,6 +345,7 @@ export interface ProviderConstructorArgs {
343
345
  export interface CreateArgs {
344
346
  config?: ModelMixConfig;
345
347
  options?: ModelMixOptions;
348
+ signal?: AbortSignal;
346
349
  outputMode?: ModelMixOutputMode;
347
350
  }
348
351
 
@@ -533,15 +536,16 @@ export declare class ModelMix {
533
536
  addImageFromUrl(url: string, options?: RoleOptions): Promise<this>;
534
537
  processImages(): Promise<void>;
535
538
 
536
- message(): Promise<string>;
539
+ message(signal?: AbortSignal): Promise<string>;
537
540
  json<T = unknown>(
538
541
  schemaExample?: T | T[] | null,
539
542
  schemaDescription?: SchemaDescription,
540
- options?: JsonMethodOptions
543
+ options?: JsonMethodOptions,
544
+ signal?: AbortSignal
541
545
  ): Promise<T>;
542
- block(options?: BlockOptions): Promise<string>;
543
- raw(): Promise<ModelMixResult>;
544
- stream(callback: StreamCallback): Promise<ModelMixResult>;
546
+ block(options?: BlockOptions, signal?: AbortSignal): Promise<string>;
547
+ raw(signal?: AbortSignal): Promise<ModelMixResult>;
548
+ stream(callback: StreamCallback, signal?: AbortSignal): Promise<ModelMixResult>;
545
549
 
546
550
  assignKeyFromFile(key: string, filePath: string): this;
547
551
  groupByRoles(messages: ChatMessage[]): ChatMessage[];
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 = {};
@@ -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;
@@ -862,21 +877,25 @@ class ModelMix {
862
877
  return this._addImageSource(source, { role, cache });
863
878
  }
864
879
 
865
- async processImages() {
866
- for (let i = 0; i < this.messages.length; i++) {
867
- const message = this.messages[i];
880
+ async processImages(signal) {
881
+ assertAbortSignal(signal);
882
+ const preparedContent = [];
883
+ for (const message of this.messages) {
868
884
  if (!Array.isArray(message.content)) continue;
885
+ const nextContent = [];
869
886
 
870
- for (let j = 0; j < message.content.length; j++) {
871
- const content = message.content[j];
872
- 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
+ }
873
892
 
874
893
  try {
875
894
  let buffer, mimeType;
876
895
 
877
896
  switch (content.source.type) {
878
897
  case 'url':
879
- const response = await fetchBinaryResponse(content.source.data);
898
+ const response = await fetchBinaryResponse(content.source.data, { signal });
880
899
  buffer = response.data;
881
900
  mimeType = response.headers['content-type'];
882
901
  break;
@@ -890,6 +909,8 @@ class ModelMix {
890
909
  break;
891
910
  }
892
911
 
912
+ throwIfAborted(signal);
913
+
893
914
  // Detect mimeType if not provided
894
915
  if (!mimeType) {
895
916
  if (typeof detectFileTypeFromBuffer !== 'function') {
@@ -902,32 +923,33 @@ class ModelMix {
902
923
  mimeType = detectedType.mime;
903
924
  }
904
925
 
905
- // Update the content with processed image
906
- message.content[j] = {
926
+ nextContent.push({
907
927
  ...content,
908
928
  source: {
909
929
  type: "base64",
910
930
  media_type: mimeType,
911
931
  data: buffer.toString('base64')
912
932
  }
913
- };
933
+ });
914
934
 
915
935
  } catch (error) {
936
+ throwIfAborted(signal);
916
937
  console.error(`Error processing image:`, error);
917
- // Remove failed image from content
918
- message.content.splice(j, 1);
919
- j--;
920
938
  }
921
939
  }
940
+ preparedContent.push({ message, content: nextContent });
922
941
  }
942
+ throwIfAborted(signal);
943
+ for (const prepared of preparedContent) prepared.message.content = prepared.content;
923
944
  }
924
945
 
925
- async message() {
926
- 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 });
927
948
  return raw.message;
928
949
  }
929
950
 
930
- 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);
931
953
 
932
954
  let isArrayWrap = false;
933
955
  if (Array.isArray(schemaExample)) {
@@ -959,7 +981,7 @@ class ModelMix {
959
981
  systemSuffix += "\n\nOutput JSON Escape: double quotes, backslashes, and control characters inside JSON strings.\nEnsure the output contains no comments.";
960
982
  }
961
983
  }
962
- const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json' });
984
+ const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json', signal });
963
985
  const parsed = JSON.parse(this._extractBlock(message));
964
986
  return isArrayWrap ? parsed.out : parsed;
965
987
  }
@@ -969,25 +991,28 @@ class ModelMix {
969
991
  return block ? block[1].trim() : response.trim();
970
992
  }
971
993
 
972
- async block({ addSystemExtra = true } = {}) {
994
+ async block({ addSystemExtra = true } = {}, signal) {
995
+ assertAbortSignal(signal);
973
996
  const systemSuffix = addSystemExtra
974
997
  ? "\nReturn the result of the task between triple backtick block code tags ```"
975
998
  : '';
976
999
  const { message } = await this.execute({
977
1000
  options: { stream: false },
978
1001
  systemSuffix,
979
- outputMode: 'block'
1002
+ outputMode: 'block',
1003
+ signal
980
1004
  });
981
1005
  return this._extractBlock(message);
982
1006
  }
983
1007
 
984
- async raw() {
985
- return this.execute({ options: { stream: false }, outputMode: 'raw' });
1008
+ async raw(signal) {
1009
+ return this.execute({ options: { stream: false }, outputMode: 'raw', signal });
986
1010
  }
987
1011
 
988
- async stream(callback) {
1012
+ async stream(callback, signal) {
1013
+ assertAbortSignal(signal);
989
1014
  this.streamCallback = callback;
990
- return this.execute({ options: { stream: true }, outputMode: 'stream' });
1015
+ return this.execute({ options: { stream: true }, outputMode: 'stream', signal });
991
1016
  }
992
1017
 
993
1018
  assignKeyFromFile(key, filePath) {
@@ -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',