modelmix 5.1.12 → 5.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -558,6 +573,9 @@ class ModelMix {
558
573
  gemini31pro({ options = {}, config = {} } = {}) {
559
574
  return this.attach('gemini-3.1-pro-preview', new MixGoogle({ options, config }));
560
575
  }
576
+ gemini38flash({ options = {}, config = {} } = {}) {
577
+ return this.attach('gemini-3.8-flash', new MixGoogle({ options, config }));
578
+ }
561
579
  gemini37flash({ options = {}, config = {} } = {}) {
562
580
  return this.attach('gemini-3.7-flash', new MixGoogle({ options, config }));
563
581
  }
@@ -862,21 +880,25 @@ class ModelMix {
862
880
  return this._addImageSource(source, { role, cache });
863
881
  }
864
882
 
865
- async processImages() {
866
- for (let i = 0; i < this.messages.length; i++) {
867
- const message = this.messages[i];
883
+ async processImages(signal) {
884
+ assertAbortSignal(signal);
885
+ const preparedContent = [];
886
+ for (const message of this.messages) {
868
887
  if (!Array.isArray(message.content)) continue;
888
+ const nextContent = [];
869
889
 
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;
890
+ for (const content of message.content) {
891
+ if (content.type !== 'image' || content.source.type === 'base64') {
892
+ nextContent.push(content);
893
+ continue;
894
+ }
873
895
 
874
896
  try {
875
897
  let buffer, mimeType;
876
898
 
877
899
  switch (content.source.type) {
878
900
  case 'url':
879
- const response = await fetchBinaryResponse(content.source.data);
901
+ const response = await fetchBinaryResponse(content.source.data, { signal });
880
902
  buffer = response.data;
881
903
  mimeType = response.headers['content-type'];
882
904
  break;
@@ -890,6 +912,8 @@ class ModelMix {
890
912
  break;
891
913
  }
892
914
 
915
+ throwIfAborted(signal);
916
+
893
917
  // Detect mimeType if not provided
894
918
  if (!mimeType) {
895
919
  if (typeof detectFileTypeFromBuffer !== 'function') {
@@ -902,32 +926,33 @@ class ModelMix {
902
926
  mimeType = detectedType.mime;
903
927
  }
904
928
 
905
- // Update the content with processed image
906
- message.content[j] = {
929
+ nextContent.push({
907
930
  ...content,
908
931
  source: {
909
932
  type: "base64",
910
933
  media_type: mimeType,
911
934
  data: buffer.toString('base64')
912
935
  }
913
- };
936
+ });
914
937
 
915
938
  } catch (error) {
939
+ throwIfAborted(signal);
916
940
  console.error(`Error processing image:`, error);
917
- // Remove failed image from content
918
- message.content.splice(j, 1);
919
- j--;
920
941
  }
921
942
  }
943
+ preparedContent.push({ message, content: nextContent });
922
944
  }
945
+ throwIfAborted(signal);
946
+ for (const prepared of preparedContent) prepared.message.content = prepared.content;
923
947
  }
924
948
 
925
- async message() {
926
- let raw = await this.execute({ options: { stream: false }, outputMode: 'message' });
949
+ async message(signal) {
950
+ let raw = await this.execute({ options: { stream: false }, outputMode: 'message', signal });
927
951
  return raw.message;
928
952
  }
929
953
 
930
- async json(schemaExample = null, schemaDescription = {}, { type = 'json_object', addExample = false, addSchema = true, addNote = false } = {}) {
954
+ async json(schemaExample = null, schemaDescription = {}, { type = 'json_object', addExample = false, addSchema = true, addNote = false } = {}, signal) {
955
+ assertAbortSignal(signal);
931
956
 
932
957
  let isArrayWrap = false;
933
958
  if (Array.isArray(schemaExample)) {
@@ -959,7 +984,7 @@ class ModelMix {
959
984
  systemSuffix += "\n\nOutput JSON Escape: double quotes, backslashes, and control characters inside JSON strings.\nEnsure the output contains no comments.";
960
985
  }
961
986
  }
962
- const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json' });
987
+ const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json', signal });
963
988
  const parsed = JSON.parse(this._extractBlock(message));
964
989
  return isArrayWrap ? parsed.out : parsed;
965
990
  }
@@ -969,25 +994,28 @@ class ModelMix {
969
994
  return block ? block[1].trim() : response.trim();
970
995
  }
971
996
 
972
- async block({ addSystemExtra = true } = {}) {
997
+ async block({ addSystemExtra = true } = {}, signal) {
998
+ assertAbortSignal(signal);
973
999
  const systemSuffix = addSystemExtra
974
1000
  ? "\nReturn the result of the task between triple backtick block code tags ```"
975
1001
  : '';
976
1002
  const { message } = await this.execute({
977
1003
  options: { stream: false },
978
1004
  systemSuffix,
979
- outputMode: 'block'
1005
+ outputMode: 'block',
1006
+ signal
980
1007
  });
981
1008
  return this._extractBlock(message);
982
1009
  }
983
1010
 
984
- async raw() {
985
- return this.execute({ options: { stream: false }, outputMode: 'raw' });
1011
+ async raw(signal) {
1012
+ return this.execute({ options: { stream: false }, outputMode: 'raw', signal });
986
1013
  }
987
1014
 
988
- async stream(callback) {
1015
+ async stream(callback, signal) {
1016
+ assertAbortSignal(signal);
989
1017
  this.streamCallback = callback;
990
- return this.execute({ options: { stream: true }, outputMode: 'stream' });
1018
+ return this.execute({ options: { stream: true }, outputMode: 'stream', signal });
991
1019
  }
992
1020
 
993
1021
  assignKeyFromFile(key, filePath) {
@@ -1144,8 +1172,8 @@ class ModelMix {
1144
1172
  }
1145
1173
  }
1146
1174
 
1147
- async prepareMessages(renderContext = createTemplateRenderContext(() => this._choiceRandom())) {
1148
- await this.processImages();
1175
+ async prepareMessages(renderContext = createTemplateRenderContext(() => this._choiceRandom()), signal) {
1176
+ await this.processImages(signal);
1149
1177
 
1150
1178
  let messages = this.messages;
1151
1179
 
@@ -1237,13 +1265,13 @@ class ModelMix {
1237
1265
  async _executePlugins({
1238
1266
  config,
1239
1267
  options,
1268
+ signal,
1240
1269
  systemSuffix,
1241
1270
  outputMode,
1242
1271
  templateContext,
1243
- executionMetadata,
1244
- isRootExecution
1272
+ executionMetadata
1245
1273
  }) {
1246
- const preparedMessages = await this.prepareMessages(templateContext);
1274
+ const preparedMessages = await this.prepareMessages(templateContext, signal);
1247
1275
  this._requirePreparedMessages(preparedMessages);
1248
1276
 
1249
1277
  const request = {
@@ -1266,6 +1294,7 @@ class ModelMix {
1266
1294
  return this.execute({
1267
1295
  config,
1268
1296
  options,
1297
+ signal,
1269
1298
  systemSuffix,
1270
1299
  outputMode,
1271
1300
  _templateContext: templateContext,
@@ -1275,6 +1304,7 @@ class ModelMix {
1275
1304
  });
1276
1305
  }
1277
1306
 
1307
+ throwIfAborted(signal);
1278
1308
  const plugin = this.plugins[index];
1279
1309
  let nextCalled = false;
1280
1310
  const next = () => {
@@ -1287,9 +1317,11 @@ class ModelMix {
1287
1317
  const context = {
1288
1318
  request,
1289
1319
  execution: Object.freeze({ ...metadata }),
1290
- invoke: input => this._invokeChild(input, metadata)
1320
+ signal,
1321
+ invoke: input => this._invokeChild(input, metadata, signal)
1291
1322
  };
1292
1323
  const result = await plugin.execute(context, next);
1324
+ throwIfAborted(signal);
1293
1325
  return validatePluginResult(result, plugin.name);
1294
1326
  };
1295
1327
 
@@ -1302,7 +1334,6 @@ class ModelMix {
1302
1334
  this._addText(result.message, { role: 'assistant' });
1303
1335
  }
1304
1336
  }
1305
- if (isRootExecution) this._commitTemplateRenderContext(templateContext);
1306
1337
  return result;
1307
1338
  }
1308
1339
 
@@ -1349,6 +1380,8 @@ class ModelMix {
1349
1380
  currentConfig.system = pluginRequest
1350
1381
  ? pluginRequest.system
1351
1382
  : this._renderSystem(config, provider.config, systemSuffix, templateContext);
1383
+ assertNoStoredSignal(currentOptions, 'options');
1384
+ assertNoStoredSignal(currentConfig, 'config');
1352
1385
 
1353
1386
  const resolvedModelKey = resolveGrok420ModelKey(
1354
1387
  currentModel.key,
@@ -1385,7 +1418,7 @@ class ModelMix {
1385
1418
  }
1386
1419
  }
1387
1420
 
1388
- async _invokeProviderWithRetry(provider, currentOptions, currentConfig, resolvedModelKey) {
1421
+ async _invokeProviderWithRetry(provider, currentOptions, currentConfig, resolvedModelKey, signal) {
1389
1422
  if (currentOptions.stream && this.streamCallback) {
1390
1423
  provider.streamCallback = this.streamCallback;
1391
1424
  }
@@ -1404,9 +1437,12 @@ class ModelMix {
1404
1437
  while (true) {
1405
1438
  const startTime = Date.now();
1406
1439
  try {
1407
- const result = await provider.create({ options: currentOptions, config: currentConfig });
1440
+ throwIfAborted(signal);
1441
+ const result = await provider.create({ options: currentOptions, config: currentConfig, signal });
1442
+ throwIfAborted(signal);
1408
1443
  return { result, elapsedMs: Date.now() - startTime };
1409
1444
  } catch (error) {
1445
+ throwIfAborted(signal);
1410
1446
  const statusCode = getErrorStatusCode(error);
1411
1447
  if (attempt >= retries || !retryableStatusCodes.has(statusCode)) throw error;
1412
1448
 
@@ -1414,7 +1450,7 @@ class ModelMix {
1414
1450
  console.log(`↺ Retrying [${resolvedModelKey}] due to status ${statusCode} (${attempt + 2}/${retries + 1})`);
1415
1451
  }
1416
1452
  const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
1417
- await sleep(delay);
1453
+ await sleepWithSignal(delay, signal);
1418
1454
  attempt += 1;
1419
1455
  }
1420
1456
  }
@@ -1438,9 +1474,10 @@ class ModelMix {
1438
1474
  }
1439
1475
 
1440
1476
  async _continueToolCalls(result, pluginRequest, execution) {
1477
+ const originalMessages = this.messages;
1441
1478
  const toolMessages = pluginRequest
1442
1479
  ? clonePluginValue(pluginRequest.messages)
1443
- : this.messages;
1480
+ : clonePluginValue(this.messages);
1444
1481
  if (result.assistantMessage) {
1445
1482
  toolMessages.push(result.assistantMessage);
1446
1483
  } else if (result.message) {
@@ -1464,7 +1501,7 @@ class ModelMix {
1464
1501
  if (!result.assistantMessage) {
1465
1502
  toolMessages.push({ role: 'assistant', content: null, tool_calls: result.toolCalls });
1466
1503
  }
1467
- const toolResults = await this.processToolCalls(result.toolCalls);
1504
+ const toolResults = await this.processToolCalls(result.toolCalls, execution.signal);
1468
1505
  for (const toolResult of toolResults) {
1469
1506
  toolMessages.push({
1470
1507
  role: 'tool',
@@ -1475,12 +1512,17 @@ class ModelMix {
1475
1512
  }
1476
1513
  this.messages = toolMessages;
1477
1514
 
1478
- return this.execute({
1479
- ...execution,
1480
- _pluginRequest: pluginRequest
1481
- ? { ...pluginRequest, messages: toolMessages }
1482
- : null
1483
- });
1515
+ try {
1516
+ return await this.execute({
1517
+ ...execution,
1518
+ _pluginRequest: pluginRequest
1519
+ ? { ...pluginRequest, messages: toolMessages }
1520
+ : null
1521
+ });
1522
+ } catch (error) {
1523
+ if (execution.signal?.aborted) this.messages = originalMessages;
1524
+ throw error;
1525
+ }
1484
1526
  }
1485
1527
 
1486
1528
  _logProviderSuccess(result, currentConfig) {
@@ -1555,6 +1597,7 @@ class ModelMix {
1555
1597
  async _executeProviderChain({
1556
1598
  config,
1557
1599
  options,
1600
+ signal,
1558
1601
  systemSuffix,
1559
1602
  outputMode,
1560
1603
  templateContext,
@@ -1564,7 +1607,7 @@ class ModelMix {
1564
1607
  }) {
1565
1608
  const preparedMessages = pluginRequest
1566
1609
  ? pluginRequest.messages
1567
- : await this.prepareMessages(templateContext);
1610
+ : await this.prepareMessages(templateContext, signal);
1568
1611
  this._requirePreparedMessages(preparedMessages);
1569
1612
 
1570
1613
  const finalConfig = pluginRequest ? pluginRequest.config : this._mergeRequestConfig(config);
@@ -1598,7 +1641,8 @@ class ModelMix {
1598
1641
  providerAttempt.provider,
1599
1642
  providerAttempt.currentOptions,
1600
1643
  providerAttempt.currentConfig,
1601
- providerAttempt.resolvedModelKey
1644
+ providerAttempt.resolvedModelKey,
1645
+ signal
1602
1646
  );
1603
1647
  this._enrichResultTokens(result, providerAttempt.resolvedModelKey, elapsedMs);
1604
1648
 
@@ -1606,6 +1650,7 @@ class ModelMix {
1606
1650
  return this._continueToolCalls(result, pluginRequest, {
1607
1651
  options,
1608
1652
  config,
1653
+ signal,
1609
1654
  systemSuffix,
1610
1655
  outputMode,
1611
1656
  _templateContext: templateContext,
@@ -1618,6 +1663,7 @@ class ModelMix {
1618
1663
  this._recordProviderResult(result);
1619
1664
  return result;
1620
1665
  } catch (error) {
1666
+ throwIfAborted(signal);
1621
1667
  lastError = error;
1622
1668
  this._logProviderFailure(error, currentModel.key, attempt, modelsToTry);
1623
1669
  }
@@ -1630,6 +1676,7 @@ class ModelMix {
1630
1676
  async execute({
1631
1677
  config = {},
1632
1678
  options = {},
1679
+ signal,
1633
1680
  systemSuffix = '',
1634
1681
  outputMode = 'raw',
1635
1682
  _templateContext = null,
@@ -1637,43 +1684,56 @@ class ModelMix {
1637
1684
  _executionMetadata = null,
1638
1685
  _pluginsApplied = false
1639
1686
  } = {}) {
1687
+ assertAbortSignal(signal);
1688
+ assertNoStoredSignal(this.config, 'config');
1689
+ assertNoStoredSignal(this.options, 'options');
1690
+ assertNoStoredSignal(config, 'config');
1691
+ assertNoStoredSignal(options, 'options');
1692
+ for (const model of this.models) {
1693
+ assertNoStoredSignal(model.provider?.config, 'provider.config');
1694
+ assertNoStoredSignal(model.provider?.options, 'provider.options');
1695
+ }
1640
1696
  const isRootExecution = _templateContext === null;
1641
1697
  const templateContext = _templateContext || createTemplateRenderContext(() => this._choiceRandom());
1698
+ let execution;
1642
1699
 
1643
1700
  if (!_pluginsApplied && this.plugins.length > 0) {
1644
- return this._executePlugins({
1701
+ execution = this._executePlugins({
1645
1702
  config,
1646
1703
  options,
1704
+ signal,
1647
1705
  systemSuffix,
1648
1706
  outputMode,
1649
1707
  templateContext,
1650
- executionMetadata: _executionMetadata,
1651
- isRootExecution
1708
+ executionMetadata: _executionMetadata
1709
+ });
1710
+ } else {
1711
+ if (!this.models || this.models.length === 0) {
1712
+ throw new Error('No models specified. Use methods like .gpt5(), .sonnet46() first.');
1713
+ }
1714
+ execution = this.limiter.schedule(() => {
1715
+ throwIfAborted(signal);
1716
+ return this._executeProviderChain({
1717
+ config,
1718
+ options,
1719
+ signal,
1720
+ systemSuffix,
1721
+ outputMode,
1722
+ templateContext,
1723
+ pluginRequest: _pluginRequest,
1724
+ executionMetadata: _executionMetadata,
1725
+ pluginsApplied: _pluginsApplied
1726
+ });
1652
1727
  });
1653
1728
  }
1654
1729
 
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);
1730
+ const result = await raceWithSignal(execution, signal);
1731
+ throwIfAborted(signal);
1732
+ if (isRootExecution) this._commitTemplateRenderContext(templateContext);
1674
1733
  return result;
1675
1734
  }
1676
- async processToolCalls(toolCalls) {
1735
+ async processToolCalls(toolCalls, signal) {
1736
+ assertAbortSignal(signal);
1677
1737
  const result = []
1678
1738
 
1679
1739
  for (const toolCall of toolCalls) {
@@ -1706,7 +1766,8 @@ class ModelMix {
1706
1766
 
1707
1767
  // Verificar si es una herramienta local registrada
1708
1768
  if (this.mcpToolsManager.hasTool(toolName)) {
1709
- const response = await this.mcpToolsManager.executeTool(toolName, toolArgs);
1769
+ const response = await this.mcpToolsManager.executeTool(toolName, toolArgs, signal);
1770
+ throwIfAborted(signal);
1710
1771
  result.push({
1711
1772
  name: toolName,
1712
1773
  tool_call_id: toolId,
@@ -1722,7 +1783,8 @@ class ModelMix {
1722
1783
  const response = await client.callTool({
1723
1784
  name: toolName,
1724
1785
  arguments: toolArgs
1725
- });
1786
+ }, undefined, signal ? { signal } : undefined);
1787
+ throwIfAborted(signal);
1726
1788
 
1727
1789
  result.push({
1728
1790
  name: toolName,
@@ -1731,6 +1793,7 @@ class ModelMix {
1731
1793
  });
1732
1794
  }
1733
1795
  } catch (error) {
1796
+ throwIfAborted(signal);
1734
1797
  console.error(`Error processing tool call ${toolName}:`, error);
1735
1798
  result.push({
1736
1799
  name: toolName || 'unknown',
@@ -0,0 +1,57 @@
1
+ function assertAbortSignal(signal) {
2
+ if (signal === undefined) return;
3
+ if (!(signal instanceof AbortSignal)) {
4
+ throw new TypeError('signal must be an AbortSignal.');
5
+ }
6
+ signal.throwIfAborted();
7
+ }
8
+
9
+ function assertNoStoredSignal(value, label) {
10
+ if (value && Object.prototype.hasOwnProperty.call(value, 'signal')) {
11
+ throw new TypeError(`${label}.signal is not supported; pass the AbortSignal to the execution method.`);
12
+ }
13
+ }
14
+
15
+ function throwIfAborted(signal) {
16
+ if (signal) signal.throwIfAborted();
17
+ }
18
+
19
+ function validateProviderExecution(provider, { config, options, signal }) {
20
+ assertAbortSignal(signal);
21
+ assertNoStoredSignal(provider.config, 'provider.config');
22
+ assertNoStoredSignal(provider.options, 'provider.options');
23
+ assertNoStoredSignal(config, 'config');
24
+ assertNoStoredSignal(options, 'options');
25
+ }
26
+
27
+ function raceWithSignal(promise, signal) {
28
+ if (!signal) return promise;
29
+ throwIfAborted(signal);
30
+ let onAbort;
31
+ const aborted = new Promise((_, reject) => {
32
+ onAbort = () => reject(signal.reason);
33
+ signal.addEventListener('abort', onAbort, { once: true });
34
+ });
35
+ return Promise.race([promise, aborted]).finally(() => {
36
+ signal.removeEventListener('abort', onAbort);
37
+ });
38
+ }
39
+
40
+ function sleepWithSignal(ms, signal) {
41
+ if (!signal) return new Promise(resolve => setTimeout(resolve, ms));
42
+ throwIfAborted(signal);
43
+ let timeout;
44
+ const sleep = new Promise(resolve => {
45
+ timeout = setTimeout(resolve, ms);
46
+ });
47
+ return raceWithSignal(sleep, signal).finally(() => clearTimeout(timeout));
48
+ }
49
+
50
+ module.exports = {
51
+ assertAbortSignal,
52
+ assertNoStoredSignal,
53
+ raceWithSignal,
54
+ sleepWithSignal,
55
+ throwIfAborted,
56
+ validateProviderExecution
57
+ };
@@ -7,7 +7,7 @@ const CHAIN_MODEL_SHORTCUTS = new Set([
7
7
  'gptRealtime', 'gptRealtimeMini', 'gpt53codex', 'gpt53chat', 'gptOss',
8
8
  'fable51', 'fable50', 'fable5', 'opus50', 'opus5', 'opus48', 'opus47', 'opus46',
9
9
  'sonnet50', 'sonnet5', 'sonnet46', 'sonnet45', 'haiku45',
10
- 'gemini31pro', 'gemini37flash', 'gemini36flash', 'gemini35flash',
10
+ 'gemini31pro', 'gemini38flash', 'gemini37flash', 'gemini36flash', 'gemini35flash',
11
11
  'gemini35flashLite', 'gemini31flashLite', 'sonarPro', 'sonar',
12
12
  'grok46', 'grok45', 'grok43', 'grok420multiAgent', 'grok420',
13
13
  'museGlimmer30b', 'museSpark12Contributor',
@@ -62,7 +62,7 @@ function createAnthropicProviders({ ModelMix, MixCustom, log }) {
62
62
  });
63
63
  }
64
64
 
65
- async create({ config = {}, options = {} } = {}) {
65
+ async create({ config = {}, options = {}, signal } = {}) {
66
66
 
67
67
  delete options.response_format;
68
68
 
@@ -85,7 +85,7 @@ function createAnthropicProviders({ ModelMix, MixCustom, log }) {
85
85
  options.system = config.system;
86
86
 
87
87
  try {
88
- return await super.create({ config: requestConfig, options });
88
+ return await super.create({ config: requestConfig, options, signal });
89
89
  } catch (error) {
90
90
  // Log the error details for debugging
91
91
  if (error.response && error.response.data) {
@@ -13,10 +13,17 @@ const {
13
13
  const { configForDebug, redactSecret } = require('../provider-debug');
14
14
  const { requireProviderApiKey } = require('../provider-api-key');
15
15
  const { normalizeOpenAIOptions } = require('./openai-options');
16
+ const {
17
+ assertNoStoredSignal,
18
+ throwIfAborted,
19
+ validateProviderExecution
20
+ } = require('../abort-signal');
16
21
 
17
22
  function createBaseProviders({ ModelMix }) {
18
23
  class MixCustom {
19
24
  constructor({ config = {}, options = {}, headers = {} } = {}) {
25
+ assertNoStoredSignal(config, 'config');
26
+ assertNoStoredSignal(options, 'options');
20
27
  this.config = this.getDefaultConfig(config);
21
28
  this.options = this.getDefaultOptions(options);
22
29
  this.headers = this.getDefaultHeaders(headers);
@@ -69,7 +76,8 @@ function createBaseProviders({ ModelMix }) {
69
76
  return buildRequestBodyAndHeaders(options, headers);
70
77
  }
71
78
 
72
- async create({ config = {}, options = {} } = {}) {
79
+ async create({ config = {}, options = {}, signal } = {}) {
80
+ validateProviderExecution(this, { config, options, signal });
73
81
  try {
74
82
  this.sanitizeCacheOptions(options);
75
83
  if (Array.isArray(options.messages)) {
@@ -90,19 +98,22 @@ function createBaseProviders({ ModelMix }) {
90
98
  }
91
99
 
92
100
  if (options.stream) {
93
- return this.processStream(await fetchStreamResponse(this.config.url, {
101
+ return await this.processStream(await fetchStreamResponse(this.config.url, {
94
102
  method: 'POST',
95
103
  headers: request.headers,
96
- body: request.body
104
+ body: request.body,
105
+ signal
97
106
  }));
98
107
  } else {
99
108
  return this.processResponse(await fetchJsonResponse(this.config.url, {
100
109
  method: 'POST',
101
110
  headers: request.headers,
102
- body: request.body
111
+ body: request.body,
112
+ signal
103
113
  }));
104
114
  }
105
115
  } catch (error) {
116
+ throwIfAborted(signal);
106
117
  throw this.handleError(error);
107
118
  }
108
119
  }
@@ -285,9 +296,9 @@ function createBaseProviders({ ModelMix }) {
285
296
  });
286
297
  }
287
298
 
288
- async create({ config = {}, options = {} } = {}) {
299
+ async create({ config = {}, options = {}, signal } = {}) {
289
300
  normalizeOpenAIOptions(options);
290
- return super.create({ config, options });
301
+ return super.create({ config, options, signal });
291
302
  }
292
303
 
293
304
  static convertMessages(messages, config) {