modelmix 5.0.2 → 5.0.4
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 +121 -7
- package/RLM_PLUGIN_SPEC.md +465 -0
- package/demo/gemini.js +3 -4
- package/demo/moderation.js +17 -0
- package/demo/short.js +1 -1
- package/effort.js +2 -0
- package/index.d.ts +107 -1
- package/index.js +443 -46
- package/package.json +4 -2
- 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 +26 -4
- package/test/effort.test.js +14 -1
- package/test/live.mcp.js +6 -6
- package/test/live.test.js +2 -2
- package/test/moderation.test.js +135 -0
- 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) {
|
|
@@ -315,7 +344,8 @@ const MODEL_PRICING = {
|
|
|
315
344
|
'gemini-3.1-pro-preview': { input: 2.00, output: 12.00 },
|
|
316
345
|
'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
|
|
317
346
|
'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
|
|
318
|
-
'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 },
|
|
319
349
|
'gemini-3.5-flash': { input: 0.75, output: 4.50 },
|
|
320
350
|
'gemini-3.5-flash-lite': { input: 0.30, output: 2.50 },
|
|
321
351
|
'gemini-2.5-pro': { input: 1.25, output: 10.00 },
|
|
@@ -380,6 +410,7 @@ class ModelMix {
|
|
|
380
410
|
this.toolClient = {};
|
|
381
411
|
this.mcp = {};
|
|
382
412
|
this.mcpToolsManager = new MCPToolsManager();
|
|
413
|
+
this.plugins = [];
|
|
383
414
|
this.templateFileAssignments = new Map();
|
|
384
415
|
this.messageTemplates = new WeakMap();
|
|
385
416
|
this.lastRaw = null;
|
|
@@ -452,6 +483,23 @@ class ModelMix {
|
|
|
452
483
|
return this;
|
|
453
484
|
}
|
|
454
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
|
+
|
|
455
503
|
static new({ options = {}, config = {}, mix = {} } = {}) {
|
|
456
504
|
return new ModelMix({ options, config, mix });
|
|
457
505
|
}
|
|
@@ -467,6 +515,7 @@ class ModelMix {
|
|
|
467
515
|
instance.systemTemplate = { ...this.systemTemplate };
|
|
468
516
|
}
|
|
469
517
|
instance.templateFileAssignments = new Map(this.templateFileAssignments);
|
|
518
|
+
instance.plugins = [...this.plugins];
|
|
470
519
|
for (const key of Object.keys(config.templateData || {})) {
|
|
471
520
|
instance.templateFileAssignments.delete(key);
|
|
472
521
|
}
|
|
@@ -474,6 +523,102 @@ class ModelMix {
|
|
|
474
523
|
return instance;
|
|
475
524
|
}
|
|
476
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
|
+
|
|
477
622
|
static formatJSON(obj) {
|
|
478
623
|
return inspect(obj, {
|
|
479
624
|
depth: null,
|
|
@@ -500,10 +645,11 @@ class ModelMix {
|
|
|
500
645
|
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
|
501
646
|
}
|
|
502
647
|
|
|
503
|
-
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 } = {}) {
|
|
504
649
|
const tokenCount = value => Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
505
650
|
const normalizedInput = tokenCount(input);
|
|
506
651
|
const normalizedOutput = tokenCount(output);
|
|
652
|
+
const normalizedThinking = tokenCount(thinking);
|
|
507
653
|
const normalizedCached = tokenCount(cached);
|
|
508
654
|
const normalizedCacheWrite5m = tokenCount(cacheWrite5m);
|
|
509
655
|
const normalizedCacheWrite1h = tokenCount(cacheWrite1h);
|
|
@@ -513,7 +659,7 @@ class ModelMix {
|
|
|
513
659
|
);
|
|
514
660
|
const normalizedTotal = Number.isFinite(total)
|
|
515
661
|
? Math.max(0, total)
|
|
516
|
-
: normalizedInput + normalizedOutput;
|
|
662
|
+
: normalizedInput + normalizedOutput + normalizedThinking;
|
|
517
663
|
const uncachedInput = Math.max(0, normalizedInput - normalizedCached - normalizedCacheWrite);
|
|
518
664
|
const cacheHitRate = normalizedInput > 0
|
|
519
665
|
? Number((normalizedCached / normalizedInput).toFixed(4))
|
|
@@ -522,6 +668,7 @@ class ModelMix {
|
|
|
522
668
|
return {
|
|
523
669
|
input: normalizedInput,
|
|
524
670
|
output: normalizedOutput,
|
|
671
|
+
thinking: normalizedThinking,
|
|
525
672
|
total: normalizedTotal,
|
|
526
673
|
cached: normalizedCached,
|
|
527
674
|
cacheWrite: normalizedCacheWrite,
|
|
@@ -581,7 +728,9 @@ class ModelMix {
|
|
|
581
728
|
cacheWrite: roundCost(genericCacheWriteCost + cacheWrite5mCost + cacheWrite1hCost),
|
|
582
729
|
cacheWrite5m: cacheWrite5mCost,
|
|
583
730
|
cacheWrite1h: cacheWrite1hCost,
|
|
584
|
-
output: roundCost(
|
|
731
|
+
output: roundCost(
|
|
732
|
+
(normalized.output + normalized.thinking) * outputPerMillion * outputMultiplier / 1_000_000
|
|
733
|
+
)
|
|
585
734
|
};
|
|
586
735
|
breakdown.total = roundCost(
|
|
587
736
|
breakdown.uncachedInput
|
|
@@ -837,6 +986,9 @@ class ModelMix {
|
|
|
837
986
|
gemini3flash({ options = {}, config = {} } = {}) {
|
|
838
987
|
return this.attach('gemini-3-flash-preview', new MixGoogle({ options, config }));
|
|
839
988
|
}
|
|
989
|
+
gemini37flash({ options = {}, config = {} } = {}) {
|
|
990
|
+
return this.attach('gemini-3.7-flash', new MixGoogle({ options, config }));
|
|
991
|
+
}
|
|
840
992
|
gemini36flash({ options = {}, config = {} } = {}) {
|
|
841
993
|
return this.attach('gemini-3.6-flash', new MixGoogle({ options, config }));
|
|
842
994
|
}
|
|
@@ -1192,7 +1344,7 @@ class ModelMix {
|
|
|
1192
1344
|
}
|
|
1193
1345
|
|
|
1194
1346
|
async message() {
|
|
1195
|
-
let raw = await this.execute({ options: { stream: false } });
|
|
1347
|
+
let raw = await this.execute({ options: { stream: false }, outputMode: 'message' });
|
|
1196
1348
|
return raw.message;
|
|
1197
1349
|
}
|
|
1198
1350
|
|
|
@@ -1228,7 +1380,7 @@ class ModelMix {
|
|
|
1228
1380
|
systemSuffix += "\n\nOutput JSON Escape: double quotes, backslashes, and control characters inside JSON strings.\nEnsure the output contains no comments.";
|
|
1229
1381
|
}
|
|
1230
1382
|
}
|
|
1231
|
-
const { message } = await this.execute({ options, config, systemSuffix });
|
|
1383
|
+
const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json' });
|
|
1232
1384
|
const parsed = JSON.parse(this._extractBlock(message));
|
|
1233
1385
|
return isArrayWrap ? parsed.out : parsed;
|
|
1234
1386
|
}
|
|
@@ -1244,18 +1396,19 @@ class ModelMix {
|
|
|
1244
1396
|
: '';
|
|
1245
1397
|
const { message } = await this.execute({
|
|
1246
1398
|
options: { stream: false },
|
|
1247
|
-
systemSuffix
|
|
1399
|
+
systemSuffix,
|
|
1400
|
+
outputMode: 'block'
|
|
1248
1401
|
});
|
|
1249
1402
|
return this._extractBlock(message);
|
|
1250
1403
|
}
|
|
1251
1404
|
|
|
1252
1405
|
async raw() {
|
|
1253
|
-
return this.execute({ options: { stream: false } });
|
|
1406
|
+
return this.execute({ options: { stream: false }, outputMode: 'raw' });
|
|
1254
1407
|
}
|
|
1255
1408
|
|
|
1256
1409
|
async stream(callback) {
|
|
1257
1410
|
this.streamCallback = callback;
|
|
1258
|
-
return this.execute({ options: { stream: true } });
|
|
1411
|
+
return this.execute({ options: { stream: true }, outputMode: 'stream' });
|
|
1259
1412
|
}
|
|
1260
1413
|
|
|
1261
1414
|
assignKeyFromFile(key, filePath) {
|
|
@@ -1474,22 +1627,25 @@ class ModelMix {
|
|
|
1474
1627
|
return this.systemTemplate;
|
|
1475
1628
|
}
|
|
1476
1629
|
|
|
1477
|
-
async execute({
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
+
} = {}) {
|
|
1482
1640
|
const isRootExecution = _templateContext === null;
|
|
1483
1641
|
const templateContext = _templateContext || createTemplateRenderContext(() => this._choiceRandom());
|
|
1484
|
-
const execution = this.limiter.schedule(async () => {
|
|
1485
|
-
const preparedMessages = await this.prepareMessages(templateContext);
|
|
1486
1642
|
|
|
1643
|
+
if (!_pluginsApplied && this.plugins.length > 0) {
|
|
1644
|
+
const preparedMessages = await this.prepareMessages(templateContext);
|
|
1487
1645
|
if (preparedMessages.length === 0) {
|
|
1488
1646
|
throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
|
|
1489
1647
|
}
|
|
1490
|
-
|
|
1491
|
-
// Merge config to get final roundRobin value and retry settings
|
|
1492
|
-
const finalConfig = {
|
|
1648
|
+
const requestConfig = {
|
|
1493
1649
|
...this.config,
|
|
1494
1650
|
...config,
|
|
1495
1651
|
retry: {
|
|
@@ -1497,6 +1653,101 @@ class ModelMix {
|
|
|
1497
1653
|
...(config.retry || {})
|
|
1498
1654
|
}
|
|
1499
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
|
+
};
|
|
1500
1751
|
|
|
1501
1752
|
// Try all models in order (first is primary, rest are fallbacks)
|
|
1502
1753
|
const modelsToTry = this.models.map((model, index) => ({ model, index }));
|
|
@@ -1523,31 +1774,45 @@ class ModelMix {
|
|
|
1523
1774
|
...providerInstance.options,
|
|
1524
1775
|
...optionsTools,
|
|
1525
1776
|
...options,
|
|
1777
|
+
...(_pluginRequest?.options || {}),
|
|
1526
1778
|
model: currentModelKey
|
|
1527
1779
|
};
|
|
1528
1780
|
|
|
1529
|
-
const currentConfig =
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1781
|
+
const currentConfig = _pluginRequest
|
|
1782
|
+
? {
|
|
1783
|
+
...providerInstance.config,
|
|
1784
|
+
..._pluginRequest.config,
|
|
1785
|
+
retry: {
|
|
1786
|
+
...(providerInstance.config?.retry || {}),
|
|
1787
|
+
...(_pluginRequest.config.retry || {})
|
|
1788
|
+
}
|
|
1537
1789
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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;
|
|
1549
1815
|
}
|
|
1550
|
-
currentConfig.system = templateContext.renderedSystems.get(systemCacheKey) + systemSuffix;
|
|
1551
1816
|
|
|
1552
1817
|
// Grok 4.20 alias → reasoning / non-reasoning from unified effort
|
|
1553
1818
|
const resolvedModelKey = resolveGrok420ModelKey(
|
|
@@ -1641,11 +1906,14 @@ class ModelMix {
|
|
|
1641
1906
|
}
|
|
1642
1907
|
|
|
1643
1908
|
if (result.toolCalls && result.toolCalls.length > 0) {
|
|
1909
|
+
const toolMessages = _pluginRequest
|
|
1910
|
+
? clonePluginValue(_pluginRequest.messages)
|
|
1911
|
+
: this.messages;
|
|
1644
1912
|
if (result.assistantMessage) {
|
|
1645
|
-
|
|
1913
|
+
toolMessages.push(result.assistantMessage);
|
|
1646
1914
|
} else if (result.message) {
|
|
1647
1915
|
if (result.signature) {
|
|
1648
|
-
|
|
1916
|
+
toolMessages.push({
|
|
1649
1917
|
role: "assistant", content: [{
|
|
1650
1918
|
type: "thinking",
|
|
1651
1919
|
// Empty string is valid (Anthropic display: "omitted").
|
|
@@ -1654,25 +1922,44 @@ class ModelMix {
|
|
|
1654
1922
|
}]
|
|
1655
1923
|
});
|
|
1656
1924
|
} else {
|
|
1657
|
-
|
|
1925
|
+
toolMessages.push({
|
|
1926
|
+
role: 'assistant',
|
|
1927
|
+
content: [{ type: 'text', text: result.message }]
|
|
1928
|
+
});
|
|
1658
1929
|
}
|
|
1659
1930
|
}
|
|
1660
1931
|
|
|
1661
1932
|
if (!result.assistantMessage) {
|
|
1662
|
-
|
|
1933
|
+
toolMessages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
|
|
1663
1934
|
}
|
|
1664
1935
|
|
|
1665
1936
|
const toolResults = await this.processToolCalls(result.toolCalls);
|
|
1666
1937
|
for (const toolResult of toolResults) {
|
|
1667
|
-
|
|
1938
|
+
toolMessages.push({
|
|
1668
1939
|
role: 'tool',
|
|
1669
1940
|
tool_call_id: toolResult.tool_call_id,
|
|
1670
1941
|
name: toolResult.name,
|
|
1671
1942
|
content: toolResult.content
|
|
1672
1943
|
});
|
|
1673
1944
|
}
|
|
1945
|
+
this.messages = toolMessages;
|
|
1674
1946
|
|
|
1675
|
-
|
|
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
|
+
});
|
|
1676
1963
|
}
|
|
1677
1964
|
|
|
1678
1965
|
// debug level 1: Just success indicator
|
|
@@ -2315,6 +2602,12 @@ class MixOpenAI extends MixCustom {
|
|
|
2315
2602
|
}
|
|
2316
2603
|
}
|
|
2317
2604
|
|
|
2605
|
+
class MixModeration extends MixCustom {
|
|
2606
|
+
getOptionsTools() {
|
|
2607
|
+
return {};
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2318
2611
|
class MixOpenAIResponses extends MixOpenAI {
|
|
2319
2612
|
async create({ config = {}, options = {} } = {}) {
|
|
2320
2613
|
|
|
@@ -2546,6 +2839,109 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
2546
2839
|
}
|
|
2547
2840
|
}
|
|
2548
2841
|
|
|
2842
|
+
class MixOpenAIModeration extends MixModeration {
|
|
2843
|
+
getDefaultConfig(customConfig) {
|
|
2844
|
+
const apiKey = customConfig.apiKey || process.env.OPENAI_API_KEY;
|
|
2845
|
+
if (!apiKey) {
|
|
2846
|
+
throw new Error('OpenAI API key not found. Please provide it in config or set OPENAI_API_KEY environment variable.');
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
return super.getDefaultConfig({
|
|
2850
|
+
url: 'https://api.openai.com/v1/moderations',
|
|
2851
|
+
apiKey,
|
|
2852
|
+
...customConfig
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
async create({ config = {}, options = {} } = {}) {
|
|
2857
|
+
if (options.stream) {
|
|
2858
|
+
throw new Error('Stream is not supported for OpenAI moderation');
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
const input = MixOpenAIModeration.messagesToModerationInput(options.messages);
|
|
2862
|
+
const response = await fetchJsonResponse(this.config.url, {
|
|
2863
|
+
method: 'POST',
|
|
2864
|
+
headers: this.headers,
|
|
2865
|
+
body: JSON.stringify({ model: options.model, input })
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
return {
|
|
2869
|
+
moderation: response.data.results,
|
|
2870
|
+
tokens: ModelMix.normalizeTokenUsage(),
|
|
2871
|
+
response: response.data
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
static messagesToModerationInput(messages = []) {
|
|
2876
|
+
const input = [];
|
|
2877
|
+
|
|
2878
|
+
for (const message of messages) {
|
|
2879
|
+
if (typeof message.content === 'string') {
|
|
2880
|
+
input.push({ type: 'text', text: message.content });
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
if (!Array.isArray(message.content)) continue;
|
|
2884
|
+
|
|
2885
|
+
for (const content of message.content) {
|
|
2886
|
+
if (content?.type === 'text') {
|
|
2887
|
+
input.push({ type: 'text', text: content.text });
|
|
2888
|
+
} else if (content?.type === 'image') {
|
|
2889
|
+
const { media_type: mediaType, data } = content.source || {};
|
|
2890
|
+
if (!mediaType || !data) {
|
|
2891
|
+
throw new Error('OpenAI moderation images must be prepared as base64 data URLs');
|
|
2892
|
+
}
|
|
2893
|
+
input.push({
|
|
2894
|
+
type: 'image_url',
|
|
2895
|
+
image_url: { url: `data:${mediaType};base64,${data}` }
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
return input;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
class ModerationMix extends ModelMix {
|
|
2906
|
+
static new(setup = {}) {
|
|
2907
|
+
return new ModerationMix(setup);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
new({ options = {}, config = {} } = {}) {
|
|
2911
|
+
return new ModerationMix({
|
|
2912
|
+
options: { ...this.options, ...options },
|
|
2913
|
+
config: { ...this.config, ...config }
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
attach(key, provider) {
|
|
2918
|
+
if (!(provider instanceof MixModeration)) {
|
|
2919
|
+
throw new Error('ModerationMix only accepts moderation providers.');
|
|
2920
|
+
}
|
|
2921
|
+
return super.attach(key, provider);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
openai({ options = {}, config = {} } = {}) {
|
|
2925
|
+
return this.attach('omni-moderation-latest', new MixOpenAIModeration({ options, config }));
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
async message() {
|
|
2929
|
+
throw new Error('ModerationMix does not generate messages. Use raw() and read result.moderation.');
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
async json() {
|
|
2933
|
+
throw new Error('ModerationMix does not generate JSON. Use raw() and read result.moderation.');
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
async block() {
|
|
2937
|
+
throw new Error('ModerationMix does not generate blocks. Use raw() and read result.moderation.');
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
async stream() {
|
|
2941
|
+
throw new Error('ModerationMix does not support streaming. Use raw().');
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2549
2945
|
class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
2550
2946
|
getDefaultConfig(customConfig) {
|
|
2551
2947
|
return super.getDefaultConfig({
|
|
@@ -3680,6 +4076,7 @@ class MixGoogle extends MixCustom {
|
|
|
3680
4076
|
return ModelMix.normalizeTokenUsage({
|
|
3681
4077
|
input: data.usageMetadata.promptTokenCount || 0,
|
|
3682
4078
|
output: data.usageMetadata.candidatesTokenCount || 0,
|
|
4079
|
+
thinking: data.usageMetadata.thoughtsTokenCount || 0,
|
|
3683
4080
|
total: data.usageMetadata.totalTokenCount,
|
|
3684
4081
|
cached: ModelMix.extractCacheTokens(data.usageMetadata),
|
|
3685
4082
|
cacheWrite: ModelMix.extractCacheWriteTokens(data.usageMetadata)
|
|
@@ -3732,4 +4129,4 @@ class MixGoogle extends MixCustom {
|
|
|
3732
4129
|
}
|
|
3733
4130
|
}
|
|
3734
4131
|
|
|
3735
|
-
module.exports = { MixCustom, ModelMix, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
|
|
4132
|
+
module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
|