modelmix 5.0.1 → 5.0.3
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 +111 -8
- package/RLM_PLUGIN_SPEC.md +465 -0
- package/demo/gemini.js +3 -4
- package/demo/grok.js +2 -2
- package/demo/images.js +2 -2
- package/demo/short.js +3 -3
- package/effort.js +3 -0
- package/index.d.ts +62 -1
- package/index.js +355 -49
- package/package.json +7 -4
- package/plugins/rlm/index.d.ts +194 -0
- package/plugins/rlm/index.js +25 -0
- package/plugins/rlm/lib/budget.js +153 -0
- package/plugins/rlm/lib/isolated-vm-sandbox.js +90 -0
- package/plugins/rlm/lib/markdown.js +156 -0
- package/plugins/rlm/lib/planner-prompt.js +137 -0
- package/plugins/rlm/lib/plugin.js +203 -0
- package/plugins/rlm/lib/runtime.js +146 -0
- package/plugins/rlm/lib/variable-descriptors.js +228 -0
- package/plugins/rlm/lib/worker-catalog.js +70 -0
- package/plugins/rlm/package.json +32 -0
- package/plugins/rlm/prompts/partials/processing-rules.md +8 -0
- package/plugins/rlm/prompts/planner.md +53 -0
- package/plugins/rlm/test/budget.test.js +86 -0
- package/plugins/rlm/test/fixtures/book.md +24 -0
- package/plugins/rlm/test/isolated-vm-sandbox.test.js +114 -0
- package/plugins/rlm/test/markdown.test.js +64 -0
- package/plugins/rlm/test/planner-template.test.js +140 -0
- package/plugins/rlm/test/plugin-contract.test.js +182 -0
- package/plugins/rlm/test/rlm-e2e.test.js +338 -0
- package/plugins/rlm/test/variable-descriptors.test.js +170 -0
- package/plugins/rlm/test/worker-catalog.test.js +104 -0
- package/pnpm-workspace.yaml +6 -0
- package/skills/modelmix/SKILL.md +23 -4
- package/test/effort.test.js +14 -1
- package/test/grok.test.js +74 -0
- package/test/live.mcp.js +8 -8
- package/test/live.test.js +9 -9
- package/test/plugins.test.js +356 -0
- package/test/tokens.test.js +37 -5
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const { randomUUID } = require('crypto');
|
|
2
3
|
const ejs = require('ejs');
|
|
3
4
|
const fileType = require('file-type');
|
|
4
5
|
const detectFileTypeFromBuffer = fileType.fileTypeFromBuffer || fileType.fromBuffer;
|
|
@@ -46,6 +47,34 @@ function isPlainObject(value) {
|
|
|
46
47
|
return prototype === Object.prototype || prototype === null;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
function clonePluginValue(value, seen = new WeakMap()) {
|
|
51
|
+
if (value === null || typeof value !== 'object') return value;
|
|
52
|
+
if (Buffer.isBuffer(value)) return Buffer.from(value);
|
|
53
|
+
if (seen.has(value)) return seen.get(value);
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
const clone = [];
|
|
57
|
+
seen.set(value, clone);
|
|
58
|
+
for (const item of value) clone.push(clonePluginValue(item, seen));
|
|
59
|
+
return clone;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!isPlainObject(value)) return value;
|
|
63
|
+
const clone = {};
|
|
64
|
+
seen.set(value, clone);
|
|
65
|
+
for (const [key, item] of Object.entries(value)) {
|
|
66
|
+
clone[key] = clonePluginValue(item, seen);
|
|
67
|
+
}
|
|
68
|
+
return clone;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validatePluginResult(result, pluginName) {
|
|
72
|
+
if (!isPlainObject(result)) {
|
|
73
|
+
throw new TypeError(`Plugin "${pluginName}" must return a ModelMixResult object.`);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
function normalizeContentCache(cache) {
|
|
50
79
|
if (cache !== undefined) {
|
|
51
80
|
if (!isPlainObject(cache) || cache.breakpoint !== true) {
|
|
@@ -259,6 +288,21 @@ const GPT56_LONG_CONTEXT_PRICING = Object.freeze({
|
|
|
259
288
|
outputMultiplier: 1.5
|
|
260
289
|
});
|
|
261
290
|
|
|
291
|
+
const GROK46_LONG_CONTEXT_PRICING = Object.freeze({
|
|
292
|
+
inputThreshold: 200_000,
|
|
293
|
+
inputMultiplier: 2,
|
|
294
|
+
outputMultiplier: 2,
|
|
295
|
+
inclusive: true
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
function usesLongContextRates(pricing, inputTokens) {
|
|
299
|
+
const longContext = pricing.longContext;
|
|
300
|
+
if (!longContext) return false;
|
|
301
|
+
return longContext.inclusive
|
|
302
|
+
? inputTokens >= longContext.inputThreshold
|
|
303
|
+
: inputTokens > longContext.inputThreshold;
|
|
304
|
+
}
|
|
305
|
+
|
|
262
306
|
const MODEL_PRICING = {
|
|
263
307
|
// OpenAI
|
|
264
308
|
'gpt-realtime-mini': { input: 0.60, cachedInput: 0.06, output: 2.40 },
|
|
@@ -300,13 +344,15 @@ const MODEL_PRICING = {
|
|
|
300
344
|
'gemini-3.1-pro-preview': { input: 2.00, output: 12.00 },
|
|
301
345
|
'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
|
|
302
346
|
'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
|
|
303
|
-
'gemini-3.
|
|
347
|
+
'gemini-3.7-flash': { input: 0.75, cachedInput: 0.075, output: 3.75 },
|
|
348
|
+
'gemini-3.6-flash': { input: 0.75, cachedInput: 0.075, output: 3.75 },
|
|
304
349
|
'gemini-3.5-flash': { input: 0.75, output: 4.50 },
|
|
305
350
|
'gemini-3.5-flash-lite': { input: 0.30, output: 2.50 },
|
|
306
351
|
'gemini-2.5-pro': { input: 1.25, output: 10.00 },
|
|
307
352
|
'gemini-2.5-flash': { input: 0.30, output: 2.50 },
|
|
308
353
|
'gemini-3.1-flash-lite-preview': { input: 0.25, output: 1.50 },
|
|
309
354
|
// Grok
|
|
355
|
+
'grok-4.6': { input: 2.00, cachedInput: 0.50, output: 6.00, longContext: GROK46_LONG_CONTEXT_PRICING },
|
|
310
356
|
'grok-4.5': { input: 2.00, output: 6.00 },
|
|
311
357
|
'grok-4.3': { input: 1.25, output: 2.50 },
|
|
312
358
|
'grok-4.20-multi-agent-0309': { input: 1.25, output: 2.50 },
|
|
@@ -364,6 +410,7 @@ class ModelMix {
|
|
|
364
410
|
this.toolClient = {};
|
|
365
411
|
this.mcp = {};
|
|
366
412
|
this.mcpToolsManager = new MCPToolsManager();
|
|
413
|
+
this.plugins = [];
|
|
367
414
|
this.templateFileAssignments = new Map();
|
|
368
415
|
this.messageTemplates = new WeakMap();
|
|
369
416
|
this.lastRaw = null;
|
|
@@ -436,6 +483,23 @@ class ModelMix {
|
|
|
436
483
|
return this;
|
|
437
484
|
}
|
|
438
485
|
|
|
486
|
+
use(plugin) {
|
|
487
|
+
if (!isPlainObject(plugin)) {
|
|
488
|
+
throw new TypeError('plugin must be a plain object.');
|
|
489
|
+
}
|
|
490
|
+
if (typeof plugin.name !== 'string' || plugin.name.trim().length === 0) {
|
|
491
|
+
throw new TypeError('plugin.name must be a non-empty string.');
|
|
492
|
+
}
|
|
493
|
+
if (typeof plugin.execute !== 'function') {
|
|
494
|
+
throw new TypeError(`Plugin "${plugin.name}" must define execute(context, next).`);
|
|
495
|
+
}
|
|
496
|
+
if (this.plugins.some(current => current.name === plugin.name)) {
|
|
497
|
+
throw new Error(`Plugin "${plugin.name}" is already registered on this instance.`);
|
|
498
|
+
}
|
|
499
|
+
this.plugins.push(plugin);
|
|
500
|
+
return this;
|
|
501
|
+
}
|
|
502
|
+
|
|
439
503
|
static new({ options = {}, config = {}, mix = {} } = {}) {
|
|
440
504
|
return new ModelMix({ options, config, mix });
|
|
441
505
|
}
|
|
@@ -451,6 +515,7 @@ class ModelMix {
|
|
|
451
515
|
instance.systemTemplate = { ...this.systemTemplate };
|
|
452
516
|
}
|
|
453
517
|
instance.templateFileAssignments = new Map(this.templateFileAssignments);
|
|
518
|
+
instance.plugins = [...this.plugins];
|
|
454
519
|
for (const key of Object.keys(config.templateData || {})) {
|
|
455
520
|
instance.templateFileAssignments.delete(key);
|
|
456
521
|
}
|
|
@@ -458,6 +523,102 @@ class ModelMix {
|
|
|
458
523
|
return instance;
|
|
459
524
|
}
|
|
460
525
|
|
|
526
|
+
_pluginsForPolicy(policy = 'inherit') {
|
|
527
|
+
if (policy === 'inherit') return [...this.plugins];
|
|
528
|
+
if (policy === 'none') return [];
|
|
529
|
+
if (!isPlainObject(policy)) {
|
|
530
|
+
throw new TypeError('plugins must be "inherit", "none", { include }, or { exclude }.');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const hasInclude = Object.prototype.hasOwnProperty.call(policy, 'include');
|
|
534
|
+
const hasExclude = Object.prototype.hasOwnProperty.call(policy, 'exclude');
|
|
535
|
+
if (hasInclude === hasExclude) {
|
|
536
|
+
throw new TypeError('plugins policy must define exactly one of include or exclude.');
|
|
537
|
+
}
|
|
538
|
+
const names = hasInclude ? policy.include : policy.exclude;
|
|
539
|
+
if (!Array.isArray(names) || names.some(name => typeof name !== 'string' || name.length === 0)) {
|
|
540
|
+
throw new TypeError('plugin include/exclude names must be non-empty strings.');
|
|
541
|
+
}
|
|
542
|
+
const uniqueNames = new Set(names);
|
|
543
|
+
const knownNames = new Set(this.plugins.map(plugin => plugin.name));
|
|
544
|
+
for (const name of uniqueNames) {
|
|
545
|
+
if (!knownNames.has(name)) {
|
|
546
|
+
throw new Error(`Plugin "${name}" is not registered on this instance.`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return hasInclude
|
|
550
|
+
? this.plugins.filter(plugin => uniqueNames.has(plugin.name))
|
|
551
|
+
: this.plugins.filter(plugin => !uniqueNames.has(plugin.name));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async _invokeChild(input, parentExecution) {
|
|
555
|
+
if (!isPlainObject(input)) {
|
|
556
|
+
throw new TypeError('Child invocation must be a plain object.');
|
|
557
|
+
}
|
|
558
|
+
if (input.history !== undefined && input.history !== false) {
|
|
559
|
+
throw new TypeError('Child invocations currently require history: false.');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const {
|
|
563
|
+
system,
|
|
564
|
+
systemFile,
|
|
565
|
+
assign,
|
|
566
|
+
messages = [],
|
|
567
|
+
tools = [],
|
|
568
|
+
options = {},
|
|
569
|
+
config = {},
|
|
570
|
+
mix = {},
|
|
571
|
+
model = this,
|
|
572
|
+
plugins = 'inherit',
|
|
573
|
+
outputMode = 'raw'
|
|
574
|
+
} = input;
|
|
575
|
+
if (!Array.isArray(messages)) {
|
|
576
|
+
throw new TypeError('Child invocation messages must be an array.');
|
|
577
|
+
}
|
|
578
|
+
if (system !== undefined && systemFile !== undefined) {
|
|
579
|
+
throw new TypeError('Child invocation must define only one of system or systemFile.');
|
|
580
|
+
}
|
|
581
|
+
if (systemFile !== undefined && (typeof systemFile !== 'string' || systemFile.length === 0)) {
|
|
582
|
+
throw new TypeError('Child invocation systemFile must be a non-empty string.');
|
|
583
|
+
}
|
|
584
|
+
if (assign !== undefined && !isPlainObject(assign)) {
|
|
585
|
+
throw new TypeError('Child invocation assign must be a plain object.');
|
|
586
|
+
}
|
|
587
|
+
if (!Array.isArray(tools)) {
|
|
588
|
+
throw new TypeError('Child invocation tools must be an array.');
|
|
589
|
+
}
|
|
590
|
+
if (!(model instanceof ModelMix)) {
|
|
591
|
+
throw new TypeError('Child invocation model must be a ModelMix instance.');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const child = model === this
|
|
595
|
+
? ModelMix.new({ options, config, mix })
|
|
596
|
+
: model.new({ options, config, mix });
|
|
597
|
+
child.models = model.models;
|
|
598
|
+
child.plugins = this._pluginsForPolicy(plugins);
|
|
599
|
+
if (assign !== undefined) child.assign(assign);
|
|
600
|
+
if (system !== undefined) child.setSystem(system);
|
|
601
|
+
if (systemFile !== undefined) child.setSystemFromFile(systemFile);
|
|
602
|
+
child.messages = clonePluginValue(messages);
|
|
603
|
+
for (const tool of tools) {
|
|
604
|
+
if (!isPlainObject(tool) || !isPlainObject(tool.tool) || typeof tool.callback !== 'function') {
|
|
605
|
+
throw new TypeError('Child invocation tools must contain { tool, callback }.');
|
|
606
|
+
}
|
|
607
|
+
child.addTool(tool.tool, tool.callback);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const execution = {
|
|
611
|
+
executionId: randomUUID(),
|
|
612
|
+
parentExecutionId: parentExecution.executionId,
|
|
613
|
+
depth: parentExecution.depth + 1
|
|
614
|
+
};
|
|
615
|
+
const result = await child.execute({
|
|
616
|
+
outputMode,
|
|
617
|
+
_executionMetadata: execution
|
|
618
|
+
});
|
|
619
|
+
return { ...result, execution };
|
|
620
|
+
}
|
|
621
|
+
|
|
461
622
|
static formatJSON(obj) {
|
|
462
623
|
return inspect(obj, {
|
|
463
624
|
depth: null,
|
|
@@ -484,10 +645,11 @@ class ModelMix {
|
|
|
484
645
|
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
|
485
646
|
}
|
|
486
647
|
|
|
487
|
-
static normalizeTokenUsage({ input = 0, output = 0, total, cached = 0, cacheWrite = 0, cacheWrite5m = 0, cacheWrite1h = 0 } = {}) {
|
|
648
|
+
static normalizeTokenUsage({ input = 0, output = 0, thinking = 0, total, cached = 0, cacheWrite = 0, cacheWrite5m = 0, cacheWrite1h = 0 } = {}) {
|
|
488
649
|
const tokenCount = value => Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
489
650
|
const normalizedInput = tokenCount(input);
|
|
490
651
|
const normalizedOutput = tokenCount(output);
|
|
652
|
+
const normalizedThinking = tokenCount(thinking);
|
|
491
653
|
const normalizedCached = tokenCount(cached);
|
|
492
654
|
const normalizedCacheWrite5m = tokenCount(cacheWrite5m);
|
|
493
655
|
const normalizedCacheWrite1h = tokenCount(cacheWrite1h);
|
|
@@ -497,7 +659,7 @@ class ModelMix {
|
|
|
497
659
|
);
|
|
498
660
|
const normalizedTotal = Number.isFinite(total)
|
|
499
661
|
? Math.max(0, total)
|
|
500
|
-
: normalizedInput + normalizedOutput;
|
|
662
|
+
: normalizedInput + normalizedOutput + normalizedThinking;
|
|
501
663
|
const uncachedInput = Math.max(0, normalizedInput - normalizedCached - normalizedCacheWrite);
|
|
502
664
|
const cacheHitRate = normalizedInput > 0
|
|
503
665
|
? Number((normalizedCached / normalizedInput).toFixed(4))
|
|
@@ -506,6 +668,7 @@ class ModelMix {
|
|
|
506
668
|
return {
|
|
507
669
|
input: normalizedInput,
|
|
508
670
|
output: normalizedOutput,
|
|
671
|
+
thinking: normalizedThinking,
|
|
509
672
|
total: normalizedTotal,
|
|
510
673
|
cached: normalizedCached,
|
|
511
674
|
cacheWrite: normalizedCacheWrite,
|
|
@@ -535,7 +698,7 @@ class ModelMix {
|
|
|
535
698
|
|
|
536
699
|
const normalized = ModelMix.normalizeTokenUsage(tokens);
|
|
537
700
|
const longContext = pricing.longContext;
|
|
538
|
-
const useLongContextRates =
|
|
701
|
+
const useLongContextRates = usesLongContextRates(pricing, normalized.input);
|
|
539
702
|
const inputMultiplier = useLongContextRates ? longContext.inputMultiplier : 1;
|
|
540
703
|
const outputMultiplier = useLongContextRates ? longContext.outputMultiplier : 1;
|
|
541
704
|
const {
|
|
@@ -565,7 +728,9 @@ class ModelMix {
|
|
|
565
728
|
cacheWrite: roundCost(genericCacheWriteCost + cacheWrite5mCost + cacheWrite1hCost),
|
|
566
729
|
cacheWrite5m: cacheWrite5mCost,
|
|
567
730
|
cacheWrite1h: cacheWrite1hCost,
|
|
568
|
-
output: roundCost(
|
|
731
|
+
output: roundCost(
|
|
732
|
+
(normalized.output + normalized.thinking) * outputPerMillion * outputMultiplier / 1_000_000
|
|
733
|
+
)
|
|
569
734
|
};
|
|
570
735
|
breakdown.total = roundCost(
|
|
571
736
|
breakdown.uncachedInput
|
|
@@ -587,9 +752,8 @@ class ModelMix {
|
|
|
587
752
|
|
|
588
753
|
const normalized = ModelMix.normalizeTokenUsage(tokens);
|
|
589
754
|
const longContext = pricing.longContext;
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
: 1;
|
|
755
|
+
const useLongContextRates = usesLongContextRates(pricing, normalized.input);
|
|
756
|
+
const inputMultiplier = useLongContextRates ? longContext.inputMultiplier : 1;
|
|
593
757
|
const cachedInputPerMillion = pricing.cachedInput ?? pricing.input;
|
|
594
758
|
const cacheWritePerMillion = pricing.cacheWrite ?? pricing.input;
|
|
595
759
|
const cacheWrite1hPerMillion = pricing.cacheWrite1h ?? cacheWritePerMillion;
|
|
@@ -822,6 +986,9 @@ class ModelMix {
|
|
|
822
986
|
gemini3flash({ options = {}, config = {} } = {}) {
|
|
823
987
|
return this.attach('gemini-3-flash-preview', new MixGoogle({ options, config }));
|
|
824
988
|
}
|
|
989
|
+
gemini37flash({ options = {}, config = {} } = {}) {
|
|
990
|
+
return this.attach('gemini-3.7-flash', new MixGoogle({ options, config }));
|
|
991
|
+
}
|
|
825
992
|
gemini36flash({ options = {}, config = {} } = {}) {
|
|
826
993
|
return this.attach('gemini-3.6-flash', new MixGoogle({ options, config }));
|
|
827
994
|
}
|
|
@@ -844,6 +1011,9 @@ class ModelMix {
|
|
|
844
1011
|
return this.attach('sonar', new MixPerplexity({ options, config }));
|
|
845
1012
|
}
|
|
846
1013
|
|
|
1014
|
+
grok46({ options = {}, config = {} } = {}) {
|
|
1015
|
+
return this.attach('grok-4.6', new MixGrok({ options, config }));
|
|
1016
|
+
}
|
|
847
1017
|
grok45({ options = {}, config = {} } = {}) {
|
|
848
1018
|
return this.attach('grok-4.5', new MixGrok({ options, config }));
|
|
849
1019
|
}
|
|
@@ -1174,7 +1344,7 @@ class ModelMix {
|
|
|
1174
1344
|
}
|
|
1175
1345
|
|
|
1176
1346
|
async message() {
|
|
1177
|
-
let raw = await this.execute({ options: { stream: false } });
|
|
1347
|
+
let raw = await this.execute({ options: { stream: false }, outputMode: 'message' });
|
|
1178
1348
|
return raw.message;
|
|
1179
1349
|
}
|
|
1180
1350
|
|
|
@@ -1210,7 +1380,7 @@ class ModelMix {
|
|
|
1210
1380
|
systemSuffix += "\n\nOutput JSON Escape: double quotes, backslashes, and control characters inside JSON strings.\nEnsure the output contains no comments.";
|
|
1211
1381
|
}
|
|
1212
1382
|
}
|
|
1213
|
-
const { message } = await this.execute({ options, config, systemSuffix });
|
|
1383
|
+
const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json' });
|
|
1214
1384
|
const parsed = JSON.parse(this._extractBlock(message));
|
|
1215
1385
|
return isArrayWrap ? parsed.out : parsed;
|
|
1216
1386
|
}
|
|
@@ -1226,18 +1396,19 @@ class ModelMix {
|
|
|
1226
1396
|
: '';
|
|
1227
1397
|
const { message } = await this.execute({
|
|
1228
1398
|
options: { stream: false },
|
|
1229
|
-
systemSuffix
|
|
1399
|
+
systemSuffix,
|
|
1400
|
+
outputMode: 'block'
|
|
1230
1401
|
});
|
|
1231
1402
|
return this._extractBlock(message);
|
|
1232
1403
|
}
|
|
1233
1404
|
|
|
1234
1405
|
async raw() {
|
|
1235
|
-
return this.execute({ options: { stream: false } });
|
|
1406
|
+
return this.execute({ options: { stream: false }, outputMode: 'raw' });
|
|
1236
1407
|
}
|
|
1237
1408
|
|
|
1238
1409
|
async stream(callback) {
|
|
1239
1410
|
this.streamCallback = callback;
|
|
1240
|
-
return this.execute({ options: { stream: true } });
|
|
1411
|
+
return this.execute({ options: { stream: true }, outputMode: 'stream' });
|
|
1241
1412
|
}
|
|
1242
1413
|
|
|
1243
1414
|
assignKeyFromFile(key, filePath) {
|
|
@@ -1456,22 +1627,25 @@ class ModelMix {
|
|
|
1456
1627
|
return this.systemTemplate;
|
|
1457
1628
|
}
|
|
1458
1629
|
|
|
1459
|
-
async execute({
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1630
|
+
async execute({
|
|
1631
|
+
config = {},
|
|
1632
|
+
options = {},
|
|
1633
|
+
systemSuffix = '',
|
|
1634
|
+
outputMode = 'raw',
|
|
1635
|
+
_templateContext = null,
|
|
1636
|
+
_pluginRequest = null,
|
|
1637
|
+
_executionMetadata = null,
|
|
1638
|
+
_pluginsApplied = false
|
|
1639
|
+
} = {}) {
|
|
1464
1640
|
const isRootExecution = _templateContext === null;
|
|
1465
1641
|
const templateContext = _templateContext || createTemplateRenderContext(() => this._choiceRandom());
|
|
1466
|
-
const execution = this.limiter.schedule(async () => {
|
|
1467
|
-
const preparedMessages = await this.prepareMessages(templateContext);
|
|
1468
1642
|
|
|
1643
|
+
if (!_pluginsApplied && this.plugins.length > 0) {
|
|
1644
|
+
const preparedMessages = await this.prepareMessages(templateContext);
|
|
1469
1645
|
if (preparedMessages.length === 0) {
|
|
1470
1646
|
throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
|
|
1471
1647
|
}
|
|
1472
|
-
|
|
1473
|
-
// Merge config to get final roundRobin value and retry settings
|
|
1474
|
-
const finalConfig = {
|
|
1648
|
+
const requestConfig = {
|
|
1475
1649
|
...this.config,
|
|
1476
1650
|
...config,
|
|
1477
1651
|
retry: {
|
|
@@ -1479,6 +1653,101 @@ class ModelMix {
|
|
|
1479
1653
|
...(config.retry || {})
|
|
1480
1654
|
}
|
|
1481
1655
|
};
|
|
1656
|
+
const systemTemplate = this._resolveSystemTemplate(config, {});
|
|
1657
|
+
const systemCacheKey = JSON.stringify([systemTemplate.filename, systemTemplate.source]);
|
|
1658
|
+
if (!templateContext.renderedSystems.has(systemCacheKey)) {
|
|
1659
|
+
templateContext.renderedSystems.set(
|
|
1660
|
+
systemCacheKey,
|
|
1661
|
+
this._renderTemplate(systemTemplate.source, {
|
|
1662
|
+
filename: systemTemplate.filename,
|
|
1663
|
+
label: 'system template'
|
|
1664
|
+
}, templateContext)
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
const request = {
|
|
1668
|
+
system: templateContext.renderedSystems.get(systemCacheKey) + systemSuffix,
|
|
1669
|
+
messages: clonePluginValue(preparedMessages),
|
|
1670
|
+
options: clonePluginValue({ ...this.options, ...options }),
|
|
1671
|
+
config: clonePluginValue(requestConfig),
|
|
1672
|
+
outputMode
|
|
1673
|
+
};
|
|
1674
|
+
const executionMetadata = _executionMetadata || {
|
|
1675
|
+
executionId: randomUUID(),
|
|
1676
|
+
parentExecutionId: null,
|
|
1677
|
+
depth: 0
|
|
1678
|
+
};
|
|
1679
|
+
let providerInvoked = false;
|
|
1680
|
+
|
|
1681
|
+
const dispatch = async index => {
|
|
1682
|
+
if (index === this.plugins.length) {
|
|
1683
|
+
providerInvoked = true;
|
|
1684
|
+
return this.execute({
|
|
1685
|
+
config,
|
|
1686
|
+
options,
|
|
1687
|
+
systemSuffix,
|
|
1688
|
+
outputMode,
|
|
1689
|
+
_templateContext: templateContext,
|
|
1690
|
+
_pluginRequest: request,
|
|
1691
|
+
_executionMetadata: executionMetadata,
|
|
1692
|
+
_pluginsApplied: true
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
const plugin = this.plugins[index];
|
|
1697
|
+
let nextCalled = false;
|
|
1698
|
+
const next = () => {
|
|
1699
|
+
if (nextCalled) {
|
|
1700
|
+
throw new Error(`Plugin "${plugin.name}" called next() multiple times.`);
|
|
1701
|
+
}
|
|
1702
|
+
nextCalled = true;
|
|
1703
|
+
return dispatch(index + 1);
|
|
1704
|
+
};
|
|
1705
|
+
const context = {
|
|
1706
|
+
request,
|
|
1707
|
+
execution: Object.freeze({ ...executionMetadata }),
|
|
1708
|
+
invoke: input => this._invokeChild(input, executionMetadata)
|
|
1709
|
+
};
|
|
1710
|
+
const result = await plugin.execute(context, next);
|
|
1711
|
+
return validatePluginResult(result, plugin.name);
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1714
|
+
const result = await dispatch(0);
|
|
1715
|
+
this.lastRaw = result;
|
|
1716
|
+
if (!providerInvoked) {
|
|
1717
|
+
if (this.config.max_history === 0) {
|
|
1718
|
+
this.messages = [];
|
|
1719
|
+
} else if (result.message) {
|
|
1720
|
+
this._addText(result.message, { role: 'assistant' });
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
if (isRootExecution) this._commitTemplateRenderContext(templateContext);
|
|
1724
|
+
return result;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
if (!this.models || this.models.length === 0) {
|
|
1728
|
+
throw new Error("No models specified. Use methods like .gpt5(), .sonnet46() first.");
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const execution = this.limiter.schedule(async () => {
|
|
1732
|
+
const preparedMessages = _pluginRequest
|
|
1733
|
+
? _pluginRequest.messages
|
|
1734
|
+
: await this.prepareMessages(templateContext);
|
|
1735
|
+
|
|
1736
|
+
if (preparedMessages.length === 0) {
|
|
1737
|
+
throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// Merge config to get final roundRobin value and retry settings
|
|
1741
|
+
const finalConfig = _pluginRequest
|
|
1742
|
+
? _pluginRequest.config
|
|
1743
|
+
: {
|
|
1744
|
+
...this.config,
|
|
1745
|
+
...config,
|
|
1746
|
+
retry: {
|
|
1747
|
+
...(this.config.retry || {}),
|
|
1748
|
+
...(config.retry || {})
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1482
1751
|
|
|
1483
1752
|
// Try all models in order (first is primary, rest are fallbacks)
|
|
1484
1753
|
const modelsToTry = this.models.map((model, index) => ({ model, index }));
|
|
@@ -1505,31 +1774,45 @@ class ModelMix {
|
|
|
1505
1774
|
...providerInstance.options,
|
|
1506
1775
|
...optionsTools,
|
|
1507
1776
|
...options,
|
|
1777
|
+
...(_pluginRequest?.options || {}),
|
|
1508
1778
|
model: currentModelKey
|
|
1509
1779
|
};
|
|
1510
1780
|
|
|
1511
|
-
const currentConfig =
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1781
|
+
const currentConfig = _pluginRequest
|
|
1782
|
+
? {
|
|
1783
|
+
...providerInstance.config,
|
|
1784
|
+
..._pluginRequest.config,
|
|
1785
|
+
retry: {
|
|
1786
|
+
...(providerInstance.config?.retry || {}),
|
|
1787
|
+
...(_pluginRequest.config.retry || {})
|
|
1788
|
+
}
|
|
1519
1789
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1790
|
+
: {
|
|
1791
|
+
...finalConfig,
|
|
1792
|
+
...providerInstance.config,
|
|
1793
|
+
...config,
|
|
1794
|
+
retry: {
|
|
1795
|
+
...(finalConfig.retry || {}),
|
|
1796
|
+
...(providerInstance.config?.retry || {}),
|
|
1797
|
+
...(config.retry || {})
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
if (_pluginRequest) {
|
|
1801
|
+
currentConfig.system = _pluginRequest.system;
|
|
1802
|
+
} else {
|
|
1803
|
+
const systemTemplate = this._resolveSystemTemplate(config, providerInstance.config);
|
|
1804
|
+
const systemCacheKey = JSON.stringify([systemTemplate.filename, systemTemplate.source]);
|
|
1805
|
+
if (!templateContext.renderedSystems.has(systemCacheKey)) {
|
|
1806
|
+
templateContext.renderedSystems.set(
|
|
1807
|
+
systemCacheKey,
|
|
1808
|
+
this._renderTemplate(systemTemplate.source, {
|
|
1809
|
+
filename: systemTemplate.filename,
|
|
1810
|
+
label: 'system template'
|
|
1811
|
+
}, templateContext)
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
currentConfig.system = templateContext.renderedSystems.get(systemCacheKey) + systemSuffix;
|
|
1531
1815
|
}
|
|
1532
|
-
currentConfig.system = templateContext.renderedSystems.get(systemCacheKey) + systemSuffix;
|
|
1533
1816
|
|
|
1534
1817
|
// Grok 4.20 alias → reasoning / non-reasoning from unified effort
|
|
1535
1818
|
const resolvedModelKey = resolveGrok420ModelKey(
|
|
@@ -1623,11 +1906,14 @@ class ModelMix {
|
|
|
1623
1906
|
}
|
|
1624
1907
|
|
|
1625
1908
|
if (result.toolCalls && result.toolCalls.length > 0) {
|
|
1909
|
+
const toolMessages = _pluginRequest
|
|
1910
|
+
? clonePluginValue(_pluginRequest.messages)
|
|
1911
|
+
: this.messages;
|
|
1626
1912
|
if (result.assistantMessage) {
|
|
1627
|
-
|
|
1913
|
+
toolMessages.push(result.assistantMessage);
|
|
1628
1914
|
} else if (result.message) {
|
|
1629
1915
|
if (result.signature) {
|
|
1630
|
-
|
|
1916
|
+
toolMessages.push({
|
|
1631
1917
|
role: "assistant", content: [{
|
|
1632
1918
|
type: "thinking",
|
|
1633
1919
|
// Empty string is valid (Anthropic display: "omitted").
|
|
@@ -1636,25 +1922,44 @@ class ModelMix {
|
|
|
1636
1922
|
}]
|
|
1637
1923
|
});
|
|
1638
1924
|
} else {
|
|
1639
|
-
|
|
1925
|
+
toolMessages.push({
|
|
1926
|
+
role: 'assistant',
|
|
1927
|
+
content: [{ type: 'text', text: result.message }]
|
|
1928
|
+
});
|
|
1640
1929
|
}
|
|
1641
1930
|
}
|
|
1642
1931
|
|
|
1643
1932
|
if (!result.assistantMessage) {
|
|
1644
|
-
|
|
1933
|
+
toolMessages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
|
|
1645
1934
|
}
|
|
1646
1935
|
|
|
1647
1936
|
const toolResults = await this.processToolCalls(result.toolCalls);
|
|
1648
1937
|
for (const toolResult of toolResults) {
|
|
1649
|
-
|
|
1938
|
+
toolMessages.push({
|
|
1650
1939
|
role: 'tool',
|
|
1651
1940
|
tool_call_id: toolResult.tool_call_id,
|
|
1652
1941
|
name: toolResult.name,
|
|
1653
1942
|
content: toolResult.content
|
|
1654
1943
|
});
|
|
1655
1944
|
}
|
|
1945
|
+
this.messages = toolMessages;
|
|
1656
1946
|
|
|
1657
|
-
|
|
1947
|
+
const nextPluginRequest = _pluginRequest
|
|
1948
|
+
? {
|
|
1949
|
+
..._pluginRequest,
|
|
1950
|
+
messages: toolMessages
|
|
1951
|
+
}
|
|
1952
|
+
: null;
|
|
1953
|
+
return this.execute({
|
|
1954
|
+
options,
|
|
1955
|
+
config,
|
|
1956
|
+
systemSuffix,
|
|
1957
|
+
outputMode,
|
|
1958
|
+
_templateContext: templateContext,
|
|
1959
|
+
_pluginRequest: nextPluginRequest,
|
|
1960
|
+
_executionMetadata,
|
|
1961
|
+
_pluginsApplied
|
|
1962
|
+
});
|
|
1658
1963
|
}
|
|
1659
1964
|
|
|
1660
1965
|
// debug level 1: Just success indicator
|
|
@@ -3662,6 +3967,7 @@ class MixGoogle extends MixCustom {
|
|
|
3662
3967
|
return ModelMix.normalizeTokenUsage({
|
|
3663
3968
|
input: data.usageMetadata.promptTokenCount || 0,
|
|
3664
3969
|
output: data.usageMetadata.candidatesTokenCount || 0,
|
|
3970
|
+
thinking: data.usageMetadata.thoughtsTokenCount || 0,
|
|
3665
3971
|
total: data.usageMetadata.totalTokenCount,
|
|
3666
3972
|
cached: ModelMix.extractCacheTokens(data.usageMetadata),
|
|
3667
3973
|
cacheWrite: ModelMix.extractCacheWriteTokens(data.usageMetadata)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.3",
|
|
4
4
|
"description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -75,6 +75,9 @@
|
|
|
75
75
|
"test:live": "mocha test/live.test.js --timeout 10000 --require test/setup.js",
|
|
76
76
|
"test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require test/setup.js",
|
|
77
77
|
"test:tokens": "mocha test/tokens.test.js --timeout 10000 --require test/setup.js",
|
|
78
|
-
"test:
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
"test:plugins": "mocha test/plugins.test.js --timeout 10000 --require test/setup.js",
|
|
79
|
+
"test:rlm": "mocha plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js",
|
|
80
|
+
"test:offline": "mocha test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js test/grok.test.js test/plugins.test.js plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js"
|
|
81
|
+
},
|
|
82
|
+
"packageManager": "pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c"
|
|
83
|
+
}
|