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 +25 -5
- package/http-client.js +6 -6
- package/index.d.ts +10 -6
- package/index.js +133 -73
- package/lib/abort-signal.js +57 -0
- 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/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 +24 -6
- package/test/abort.test.js +517 -0
- package/test/live.test.js +9 -4
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.
|
|
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
|
-
|
|
867
|
-
|
|
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 (
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1317
|
+
signal,
|
|
1318
|
+
invoke: input => this._invokeChild(input, metadata, signal)
|
|
1291
1319
|
};
|
|
1292
1320
|
const result = await plugin.execute(context, next);
|
|
1321
|
+
throwIfAborted(signal);
|
|
1293
1322
|
return validatePluginResult(result, plugin.name);
|
|
1294
1323
|
};
|
|
1295
1324
|
|
|
@@ -1302,7 +1331,6 @@ class ModelMix {
|
|
|
1302
1331
|
this._addText(result.message, { role: 'assistant' });
|
|
1303
1332
|
}
|
|
1304
1333
|
}
|
|
1305
|
-
if (isRootExecution) this._commitTemplateRenderContext(templateContext);
|
|
1306
1334
|
return result;
|
|
1307
1335
|
}
|
|
1308
1336
|
|
|
@@ -1349,6 +1377,8 @@ class ModelMix {
|
|
|
1349
1377
|
currentConfig.system = pluginRequest
|
|
1350
1378
|
? pluginRequest.system
|
|
1351
1379
|
: this._renderSystem(config, provider.config, systemSuffix, templateContext);
|
|
1380
|
+
assertNoStoredSignal(currentOptions, 'options');
|
|
1381
|
+
assertNoStoredSignal(currentConfig, 'config');
|
|
1352
1382
|
|
|
1353
1383
|
const resolvedModelKey = resolveGrok420ModelKey(
|
|
1354
1384
|
currentModel.key,
|
|
@@ -1385,7 +1415,7 @@ class ModelMix {
|
|
|
1385
1415
|
}
|
|
1386
1416
|
}
|
|
1387
1417
|
|
|
1388
|
-
async _invokeProviderWithRetry(provider, currentOptions, currentConfig, resolvedModelKey) {
|
|
1418
|
+
async _invokeProviderWithRetry(provider, currentOptions, currentConfig, resolvedModelKey, signal) {
|
|
1389
1419
|
if (currentOptions.stream && this.streamCallback) {
|
|
1390
1420
|
provider.streamCallback = this.streamCallback;
|
|
1391
1421
|
}
|
|
@@ -1404,9 +1434,12 @@ class ModelMix {
|
|
|
1404
1434
|
while (true) {
|
|
1405
1435
|
const startTime = Date.now();
|
|
1406
1436
|
try {
|
|
1407
|
-
|
|
1437
|
+
throwIfAborted(signal);
|
|
1438
|
+
const result = await provider.create({ options: currentOptions, config: currentConfig, signal });
|
|
1439
|
+
throwIfAborted(signal);
|
|
1408
1440
|
return { result, elapsedMs: Date.now() - startTime };
|
|
1409
1441
|
} catch (error) {
|
|
1442
|
+
throwIfAborted(signal);
|
|
1410
1443
|
const statusCode = getErrorStatusCode(error);
|
|
1411
1444
|
if (attempt >= retries || !retryableStatusCodes.has(statusCode)) throw error;
|
|
1412
1445
|
|
|
@@ -1414,7 +1447,7 @@ class ModelMix {
|
|
|
1414
1447
|
console.log(`↺ Retrying [${resolvedModelKey}] due to status ${statusCode} (${attempt + 2}/${retries + 1})`);
|
|
1415
1448
|
}
|
|
1416
1449
|
const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
1417
|
-
await
|
|
1450
|
+
await sleepWithSignal(delay, signal);
|
|
1418
1451
|
attempt += 1;
|
|
1419
1452
|
}
|
|
1420
1453
|
}
|
|
@@ -1438,9 +1471,10 @@ class ModelMix {
|
|
|
1438
1471
|
}
|
|
1439
1472
|
|
|
1440
1473
|
async _continueToolCalls(result, pluginRequest, execution) {
|
|
1474
|
+
const originalMessages = this.messages;
|
|
1441
1475
|
const toolMessages = pluginRequest
|
|
1442
1476
|
? clonePluginValue(pluginRequest.messages)
|
|
1443
|
-
: this.messages;
|
|
1477
|
+
: clonePluginValue(this.messages);
|
|
1444
1478
|
if (result.assistantMessage) {
|
|
1445
1479
|
toolMessages.push(result.assistantMessage);
|
|
1446
1480
|
} else if (result.message) {
|
|
@@ -1464,7 +1498,7 @@ class ModelMix {
|
|
|
1464
1498
|
if (!result.assistantMessage) {
|
|
1465
1499
|
toolMessages.push({ role: 'assistant', content: null, tool_calls: result.toolCalls });
|
|
1466
1500
|
}
|
|
1467
|
-
const toolResults = await this.processToolCalls(result.toolCalls);
|
|
1501
|
+
const toolResults = await this.processToolCalls(result.toolCalls, execution.signal);
|
|
1468
1502
|
for (const toolResult of toolResults) {
|
|
1469
1503
|
toolMessages.push({
|
|
1470
1504
|
role: 'tool',
|
|
@@ -1475,12 +1509,17 @@ class ModelMix {
|
|
|
1475
1509
|
}
|
|
1476
1510
|
this.messages = toolMessages;
|
|
1477
1511
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1512
|
+
try {
|
|
1513
|
+
return await this.execute({
|
|
1514
|
+
...execution,
|
|
1515
|
+
_pluginRequest: pluginRequest
|
|
1516
|
+
? { ...pluginRequest, messages: toolMessages }
|
|
1517
|
+
: null
|
|
1518
|
+
});
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
if (execution.signal?.aborted) this.messages = originalMessages;
|
|
1521
|
+
throw error;
|
|
1522
|
+
}
|
|
1484
1523
|
}
|
|
1485
1524
|
|
|
1486
1525
|
_logProviderSuccess(result, currentConfig) {
|
|
@@ -1555,6 +1594,7 @@ class ModelMix {
|
|
|
1555
1594
|
async _executeProviderChain({
|
|
1556
1595
|
config,
|
|
1557
1596
|
options,
|
|
1597
|
+
signal,
|
|
1558
1598
|
systemSuffix,
|
|
1559
1599
|
outputMode,
|
|
1560
1600
|
templateContext,
|
|
@@ -1564,7 +1604,7 @@ class ModelMix {
|
|
|
1564
1604
|
}) {
|
|
1565
1605
|
const preparedMessages = pluginRequest
|
|
1566
1606
|
? pluginRequest.messages
|
|
1567
|
-
: await this.prepareMessages(templateContext);
|
|
1607
|
+
: await this.prepareMessages(templateContext, signal);
|
|
1568
1608
|
this._requirePreparedMessages(preparedMessages);
|
|
1569
1609
|
|
|
1570
1610
|
const finalConfig = pluginRequest ? pluginRequest.config : this._mergeRequestConfig(config);
|
|
@@ -1598,7 +1638,8 @@ class ModelMix {
|
|
|
1598
1638
|
providerAttempt.provider,
|
|
1599
1639
|
providerAttempt.currentOptions,
|
|
1600
1640
|
providerAttempt.currentConfig,
|
|
1601
|
-
providerAttempt.resolvedModelKey
|
|
1641
|
+
providerAttempt.resolvedModelKey,
|
|
1642
|
+
signal
|
|
1602
1643
|
);
|
|
1603
1644
|
this._enrichResultTokens(result, providerAttempt.resolvedModelKey, elapsedMs);
|
|
1604
1645
|
|
|
@@ -1606,6 +1647,7 @@ class ModelMix {
|
|
|
1606
1647
|
return this._continueToolCalls(result, pluginRequest, {
|
|
1607
1648
|
options,
|
|
1608
1649
|
config,
|
|
1650
|
+
signal,
|
|
1609
1651
|
systemSuffix,
|
|
1610
1652
|
outputMode,
|
|
1611
1653
|
_templateContext: templateContext,
|
|
@@ -1618,6 +1660,7 @@ class ModelMix {
|
|
|
1618
1660
|
this._recordProviderResult(result);
|
|
1619
1661
|
return result;
|
|
1620
1662
|
} catch (error) {
|
|
1663
|
+
throwIfAborted(signal);
|
|
1621
1664
|
lastError = error;
|
|
1622
1665
|
this._logProviderFailure(error, currentModel.key, attempt, modelsToTry);
|
|
1623
1666
|
}
|
|
@@ -1630,6 +1673,7 @@ class ModelMix {
|
|
|
1630
1673
|
async execute({
|
|
1631
1674
|
config = {},
|
|
1632
1675
|
options = {},
|
|
1676
|
+
signal,
|
|
1633
1677
|
systemSuffix = '',
|
|
1634
1678
|
outputMode = 'raw',
|
|
1635
1679
|
_templateContext = null,
|
|
@@ -1637,43 +1681,56 @@ class ModelMix {
|
|
|
1637
1681
|
_executionMetadata = null,
|
|
1638
1682
|
_pluginsApplied = false
|
|
1639
1683
|
} = {}) {
|
|
1684
|
+
assertAbortSignal(signal);
|
|
1685
|
+
assertNoStoredSignal(this.config, 'config');
|
|
1686
|
+
assertNoStoredSignal(this.options, 'options');
|
|
1687
|
+
assertNoStoredSignal(config, 'config');
|
|
1688
|
+
assertNoStoredSignal(options, 'options');
|
|
1689
|
+
for (const model of this.models) {
|
|
1690
|
+
assertNoStoredSignal(model.provider?.config, 'provider.config');
|
|
1691
|
+
assertNoStoredSignal(model.provider?.options, 'provider.options');
|
|
1692
|
+
}
|
|
1640
1693
|
const isRootExecution = _templateContext === null;
|
|
1641
1694
|
const templateContext = _templateContext || createTemplateRenderContext(() => this._choiceRandom());
|
|
1695
|
+
let execution;
|
|
1642
1696
|
|
|
1643
1697
|
if (!_pluginsApplied && this.plugins.length > 0) {
|
|
1644
|
-
|
|
1698
|
+
execution = this._executePlugins({
|
|
1645
1699
|
config,
|
|
1646
1700
|
options,
|
|
1701
|
+
signal,
|
|
1647
1702
|
systemSuffix,
|
|
1648
1703
|
outputMode,
|
|
1649
1704
|
templateContext,
|
|
1650
|
-
executionMetadata: _executionMetadata
|
|
1651
|
-
|
|
1705
|
+
executionMetadata: _executionMetadata
|
|
1706
|
+
});
|
|
1707
|
+
} else {
|
|
1708
|
+
if (!this.models || this.models.length === 0) {
|
|
1709
|
+
throw new Error('No models specified. Use methods like .gpt5(), .sonnet46() first.');
|
|
1710
|
+
}
|
|
1711
|
+
execution = this.limiter.schedule(() => {
|
|
1712
|
+
throwIfAborted(signal);
|
|
1713
|
+
return this._executeProviderChain({
|
|
1714
|
+
config,
|
|
1715
|
+
options,
|
|
1716
|
+
signal,
|
|
1717
|
+
systemSuffix,
|
|
1718
|
+
outputMode,
|
|
1719
|
+
templateContext,
|
|
1720
|
+
pluginRequest: _pluginRequest,
|
|
1721
|
+
executionMetadata: _executionMetadata,
|
|
1722
|
+
pluginsApplied: _pluginsApplied
|
|
1723
|
+
});
|
|
1652
1724
|
});
|
|
1653
1725
|
}
|
|
1654
1726
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
const execution = this.limiter.schedule(() => this._executeProviderChain({
|
|
1660
|
-
config,
|
|
1661
|
-
options,
|
|
1662
|
-
systemSuffix,
|
|
1663
|
-
outputMode,
|
|
1664
|
-
templateContext,
|
|
1665
|
-
pluginRequest: _pluginRequest,
|
|
1666
|
-
executionMetadata: _executionMetadata,
|
|
1667
|
-
pluginsApplied: _pluginsApplied
|
|
1668
|
-
}));
|
|
1669
|
-
|
|
1670
|
-
if (!isRootExecution) return execution;
|
|
1671
|
-
|
|
1672
|
-
const result = await execution;
|
|
1673
|
-
this._commitTemplateRenderContext(templateContext);
|
|
1727
|
+
const result = await raceWithSignal(execution, signal);
|
|
1728
|
+
throwIfAborted(signal);
|
|
1729
|
+
if (isRootExecution) this._commitTemplateRenderContext(templateContext);
|
|
1674
1730
|
return result;
|
|
1675
1731
|
}
|
|
1676
|
-
async processToolCalls(toolCalls) {
|
|
1732
|
+
async processToolCalls(toolCalls, signal) {
|
|
1733
|
+
assertAbortSignal(signal);
|
|
1677
1734
|
const result = []
|
|
1678
1735
|
|
|
1679
1736
|
for (const toolCall of toolCalls) {
|
|
@@ -1706,7 +1763,8 @@ class ModelMix {
|
|
|
1706
1763
|
|
|
1707
1764
|
// Verificar si es una herramienta local registrada
|
|
1708
1765
|
if (this.mcpToolsManager.hasTool(toolName)) {
|
|
1709
|
-
const response = await this.mcpToolsManager.executeTool(toolName, toolArgs);
|
|
1766
|
+
const response = await this.mcpToolsManager.executeTool(toolName, toolArgs, signal);
|
|
1767
|
+
throwIfAborted(signal);
|
|
1710
1768
|
result.push({
|
|
1711
1769
|
name: toolName,
|
|
1712
1770
|
tool_call_id: toolId,
|
|
@@ -1722,7 +1780,8 @@ class ModelMix {
|
|
|
1722
1780
|
const response = await client.callTool({
|
|
1723
1781
|
name: toolName,
|
|
1724
1782
|
arguments: toolArgs
|
|
1725
|
-
});
|
|
1783
|
+
}, undefined, signal ? { signal } : undefined);
|
|
1784
|
+
throwIfAborted(signal);
|
|
1726
1785
|
|
|
1727
1786
|
result.push({
|
|
1728
1787
|
name: toolName,
|
|
@@ -1731,6 +1790,7 @@ class ModelMix {
|
|
|
1731
1790
|
});
|
|
1732
1791
|
}
|
|
1733
1792
|
} catch (error) {
|
|
1793
|
+
throwIfAborted(signal);
|
|
1734
1794
|
console.error(`Error processing tool call ${toolName}:`, error);
|
|
1735
1795
|
result.push({
|
|
1736
1796
|
name: toolName || 'unknown',
|