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/README.md +30 -9
- package/demo/gemini.js +3 -3
- package/demo/short.js +1 -1
- package/effort.js +2 -1
- package/http-client.js +6 -6
- package/index.d.ts +11 -6
- package/index.js +136 -73
- package/lib/abort-signal.js +57 -0
- package/lib/model-chain.js +1 -1
- package/lib/providers/anthropic.js +2 -2
- package/lib/providers/base.js +17 -6
- package/lib/providers/google.js +9 -2
- package/lib/providers/openai-compatible.js +8 -8
- package/lib/providers/openai.js +32 -10
- package/lib/token-usage.js +1 -0
- package/mcp-tools.js +5 -2
- package/package.json +2 -2
- package/plugins/rlm/index.d.ts +1 -0
- package/plugins/rlm/lib/isolated-vm-sandbox.js +8 -1
- package/plugins/rlm/lib/plugin.js +23 -5
- package/plugins/rlm/test/isolated-vm-sandbox.test.js +23 -0
- package/skills/modelmix/SKILL.md +30 -12
- package/test/abort.test.js +517 -0
- package/test/effort.test.js +13 -0
- package/test/fallback.test.js +2 -2
- package/test/live.mcp.js +6 -6
- package/test/live.test.js +11 -6
- package/test/tokens.test.js +9 -6
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
|
-
|
|
867
|
-
|
|
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 (
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
+
};
|
package/lib/model-chain.js
CHANGED
|
@@ -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) {
|
package/lib/providers/base.js
CHANGED
|
@@ -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) {
|