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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const { describeVariables, plannerTemplateData } = require('..');
|
|
3
|
+
|
|
4
|
+
describe('RLM variable metadata', () => {
|
|
5
|
+
const workerManifest = {
|
|
6
|
+
fast: {
|
|
7
|
+
intelligence: 1,
|
|
8
|
+
cost: 1,
|
|
9
|
+
speed: 5,
|
|
10
|
+
description: 'Fast transformations'
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const runtimeLimits = {
|
|
14
|
+
maxCalls: 20,
|
|
15
|
+
maxOutputBytes: 1024 * 1024,
|
|
16
|
+
maxGeneratedTokens: 10000,
|
|
17
|
+
maxWallTimeMs: 30000
|
|
18
|
+
};
|
|
19
|
+
const book = {
|
|
20
|
+
title: 'A hidden title that must not reach the planner',
|
|
21
|
+
chapters: [
|
|
22
|
+
{
|
|
23
|
+
heading: 'First hidden chapter',
|
|
24
|
+
content: 'First secret paragraph.\n\nSecond secret paragraph.',
|
|
25
|
+
tags: ['opening', 'setup']
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
heading: 'Second hidden chapter',
|
|
29
|
+
content: 'Árbol and emoji 😀.\n\nAnother concealed paragraph.',
|
|
30
|
+
tags: ['middle']
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
it('describes strings, objects, and arrays without exposing their content', () => {
|
|
36
|
+
const manifest = describeVariables(book);
|
|
37
|
+
const title = manifest.descriptors.title;
|
|
38
|
+
const chapters = manifest.descriptors.chapters;
|
|
39
|
+
|
|
40
|
+
expect(manifest).to.include({
|
|
41
|
+
sizeBasis: 'serialized-json-utf8',
|
|
42
|
+
variables: 2,
|
|
43
|
+
estimatedBytes: Buffer.byteLength(JSON.stringify(book), 'utf8')
|
|
44
|
+
});
|
|
45
|
+
expect(title).to.include({
|
|
46
|
+
path: 'title',
|
|
47
|
+
type: 'string',
|
|
48
|
+
characters: Array.from(book.title).length,
|
|
49
|
+
utf16CodeUnits: book.title.length,
|
|
50
|
+
utf8Bytes: Buffer.byteLength(book.title, 'utf8'),
|
|
51
|
+
lines: 1,
|
|
52
|
+
paragraphs: 1
|
|
53
|
+
});
|
|
54
|
+
expect(chapters).to.include({
|
|
55
|
+
path: 'chapters',
|
|
56
|
+
type: 'array',
|
|
57
|
+
items: 2,
|
|
58
|
+
estimatedBytes: Buffer.byteLength(JSON.stringify(book.chapters), 'utf8')
|
|
59
|
+
});
|
|
60
|
+
expect(chapters.itemSize).to.deep.equal({
|
|
61
|
+
min: Math.min(...book.chapters.map(chapter => Buffer.byteLength(JSON.stringify(chapter), 'utf8'))),
|
|
62
|
+
max: Math.max(...book.chapters.map(chapter => Buffer.byteLength(JSON.stringify(chapter), 'utf8'))),
|
|
63
|
+
average: Number((book.chapters
|
|
64
|
+
.map(chapter => Buffer.byteLength(JSON.stringify(chapter), 'utf8'))
|
|
65
|
+
.reduce((sum, bytes) => sum + bytes, 0) / 2).toFixed(2)),
|
|
66
|
+
total: book.chapters
|
|
67
|
+
.map(chapter => Buffer.byteLength(JSON.stringify(chapter), 'utf8'))
|
|
68
|
+
.reduce((sum, bytes) => sum + bytes, 0)
|
|
69
|
+
});
|
|
70
|
+
expect(chapters.itemShape.types).to.deep.equal({ object: 2 });
|
|
71
|
+
expect(chapters.itemShape.properties.content).to.deep.include({
|
|
72
|
+
present: 2,
|
|
73
|
+
missing: 0,
|
|
74
|
+
types: { string: 2 }
|
|
75
|
+
});
|
|
76
|
+
expect(chapters.itemShape.properties.content.stringSize.paragraphs).to.deep.equal({
|
|
77
|
+
min: 2,
|
|
78
|
+
max: 2,
|
|
79
|
+
average: 2,
|
|
80
|
+
total: 4
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const serializedManifest = JSON.stringify(manifest);
|
|
84
|
+
expect(serializedManifest).to.not.include(book.title);
|
|
85
|
+
for (const chapter of book.chapters) {
|
|
86
|
+
expect(serializedManifest).to.not.include(chapter.heading);
|
|
87
|
+
expect(serializedManifest).to.not.include(chapter.content);
|
|
88
|
+
for (const tag of chapter.tags) expect(serializedManifest).to.not.include(tag);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('keeps exact aggregate sizes for large arrays without listing their items', () => {
|
|
93
|
+
const values = Array.from({ length: 100000 }, (_, index) => `row-${index}`);
|
|
94
|
+
const descriptor = describeVariables({ values }).descriptors.values;
|
|
95
|
+
|
|
96
|
+
expect(descriptor.items).to.equal(values.length);
|
|
97
|
+
expect(descriptor.estimatedBytes).to.equal(Buffer.byteLength(JSON.stringify(values), 'utf8'));
|
|
98
|
+
expect(descriptor).to.not.have.property('children');
|
|
99
|
+
expect(JSON.stringify(descriptor)).to.not.include('row-99999');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('rejects values that cannot safely enter the sandbox environment', () => {
|
|
103
|
+
const circular = {};
|
|
104
|
+
circular.self = circular;
|
|
105
|
+
|
|
106
|
+
expect(() => describeVariables({ circular })).to.throw('circular reference');
|
|
107
|
+
expect(() => describeVariables({ callback() {} })).to.throw('unsupported type function');
|
|
108
|
+
expect(() => describeVariables({ invalid: Infinity })).to.throw('finite numbers');
|
|
109
|
+
expect(() => describeVariables({ date: new Date() })).to.throw('plain objects');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('adds limits and actionable partition guidance to the planner prompt', () => {
|
|
113
|
+
const templateData = plannerTemplateData({
|
|
114
|
+
variables: book,
|
|
115
|
+
limits: {
|
|
116
|
+
...runtimeLimits,
|
|
117
|
+
maxQueryBytes: 32,
|
|
118
|
+
sandboxMemoryBytes: 64 * 1024 * 1024,
|
|
119
|
+
maxConcurrentQueries: 4
|
|
120
|
+
},
|
|
121
|
+
workerManifest
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(templateData.variableManifest).to.include('"items": 2');
|
|
125
|
+
expect(templateData.processingLimits).to.include('"maxQueryBytes": 32');
|
|
126
|
+
expect(templateData.processingLimits).to.include('"sandboxMemoryBytes": 67108864');
|
|
127
|
+
expect(templateData.processingLimits).to.include('"maxConcurrentQueries": 4');
|
|
128
|
+
expect(templateData.planningHints).to.include('"strategy": "split-oversized-items-semantically"');
|
|
129
|
+
expect(templateData.planningHints).to.include('"oversizedStringFields"');
|
|
130
|
+
expect(templateData.planningHints).to.include('"content"');
|
|
131
|
+
const serializedTemplateData = JSON.stringify(templateData);
|
|
132
|
+
expect(serializedTemplateData).to.not.include(book.title);
|
|
133
|
+
for (const chapter of book.chapters) {
|
|
134
|
+
expect(serializedTemplateData).to.not.include(chapter.heading);
|
|
135
|
+
expect(serializedTemplateData).to.not.include(chapter.content);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('distinguishes direct values from arrays that should be batched', () => {
|
|
140
|
+
const templateData = plannerTemplateData({
|
|
141
|
+
variables: {
|
|
142
|
+
instruction: 'short',
|
|
143
|
+
paragraphs: Array.from({ length: 20 }, () => 'small paragraph')
|
|
144
|
+
},
|
|
145
|
+
limits: {
|
|
146
|
+
...runtimeLimits,
|
|
147
|
+
maxQueryBytes: 80,
|
|
148
|
+
sandboxMemoryBytes: 1024 * 1024,
|
|
149
|
+
maxConcurrentQueries: 2
|
|
150
|
+
},
|
|
151
|
+
workerManifest
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(templateData.planningHints).to.include('"path": "instruction"');
|
|
155
|
+
expect(templateData.planningHints).to.include('"strategy": "direct"');
|
|
156
|
+
expect(templateData.planningHints).to.include('"path": "paragraphs"');
|
|
157
|
+
expect(templateData.planningHints).to.include('"strategy": "batch-array-items"');
|
|
158
|
+
expect(templateData.planningHints).to.match(/"suggestedMaxItemsPerQuery": [1-9]\d*/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('requires explicit planning limits instead of silent defaults', () => {
|
|
162
|
+
expect(() => plannerTemplateData({ variables: {}, limits: {}, workerManifest }))
|
|
163
|
+
.to.throw('limits.maxQueryBytes');
|
|
164
|
+
expect(() => plannerTemplateData({
|
|
165
|
+
variables: {},
|
|
166
|
+
limits: { maxQueryBytes: 100, sandboxMemoryBytes: 1000 },
|
|
167
|
+
workerManifest
|
|
168
|
+
})).to.throw('limits.maxConcurrentQueries');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const { MixCustom, ModelMix } = require('../../../index.js');
|
|
3
|
+
const { createWorkerCatalog } = require('..');
|
|
4
|
+
|
|
5
|
+
function workerModel(secret = 'worker-secret') {
|
|
6
|
+
return ModelMix.new().attach('worker-model', new MixCustom({
|
|
7
|
+
config: { apiKey: secret, url: 'https://private-provider.invalid' }
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('RLM worker catalog', () => {
|
|
12
|
+
it('exposes decision metadata without models, providers, or credentials', () => {
|
|
13
|
+
const model = workerModel();
|
|
14
|
+
const catalog = createWorkerCatalog({
|
|
15
|
+
expert: {
|
|
16
|
+
model,
|
|
17
|
+
intelligence: 5,
|
|
18
|
+
cost: 4,
|
|
19
|
+
speed: 2,
|
|
20
|
+
description: 'Complex synthesis'
|
|
21
|
+
},
|
|
22
|
+
fast: {
|
|
23
|
+
model: workerModel('another-secret'),
|
|
24
|
+
intelligence: 1,
|
|
25
|
+
cost: 1,
|
|
26
|
+
speed: 5,
|
|
27
|
+
description: 'Extraction and classification'
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(catalog.get('expert')).to.equal(model);
|
|
32
|
+
expect(catalog.manifest).to.deep.equal({
|
|
33
|
+
expert: {
|
|
34
|
+
intelligence: 5,
|
|
35
|
+
cost: 4,
|
|
36
|
+
speed: 2,
|
|
37
|
+
description: 'Complex synthesis'
|
|
38
|
+
},
|
|
39
|
+
fast: {
|
|
40
|
+
intelligence: 1,
|
|
41
|
+
cost: 1,
|
|
42
|
+
speed: 5,
|
|
43
|
+
description: 'Extraction and classification'
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const serialized = JSON.stringify(catalog.manifest);
|
|
47
|
+
expect(serialized).to.not.include('worker-secret');
|
|
48
|
+
expect(serialized).to.not.include('private-provider');
|
|
49
|
+
expect(serialized).to.not.include('models');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects invalid workers and unknown selections', () => {
|
|
53
|
+
expect(() => createWorkerCatalog({})).to.throw('non-empty');
|
|
54
|
+
expect(() => createWorkerCatalog({
|
|
55
|
+
invalid: {
|
|
56
|
+
model: {},
|
|
57
|
+
intelligence: 1,
|
|
58
|
+
cost: 1,
|
|
59
|
+
speed: 1,
|
|
60
|
+
description: 'Invalid model'
|
|
61
|
+
}
|
|
62
|
+
})).to.throw('ModelMix instance');
|
|
63
|
+
const catalog = createWorkerCatalog({
|
|
64
|
+
valid: {
|
|
65
|
+
model: workerModel(),
|
|
66
|
+
intelligence: 1,
|
|
67
|
+
cost: 1,
|
|
68
|
+
speed: 1,
|
|
69
|
+
description: 'Valid worker'
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
expect(() => catalog.get('missing')).to.throw('Unknown RLM worker');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('represents the inherited parent chain without exposing a model object', () => {
|
|
76
|
+
const catalog = createWorkerCatalog({
|
|
77
|
+
parent: {
|
|
78
|
+
useParent: true,
|
|
79
|
+
intelligence: 3,
|
|
80
|
+
cost: 2,
|
|
81
|
+
speed: 3,
|
|
82
|
+
description: 'Use the current ModelMix chain'
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(catalog.get('parent')).to.equal(undefined);
|
|
87
|
+
expect(catalog.manifest.parent).to.deep.equal({
|
|
88
|
+
intelligence: 3,
|
|
89
|
+
cost: 2,
|
|
90
|
+
speed: 3,
|
|
91
|
+
description: 'Use the current ModelMix chain'
|
|
92
|
+
});
|
|
93
|
+
expect(() => createWorkerCatalog({
|
|
94
|
+
invalid: {
|
|
95
|
+
model: workerModel(),
|
|
96
|
+
useParent: true,
|
|
97
|
+
intelligence: 1,
|
|
98
|
+
cost: 1,
|
|
99
|
+
speed: 1,
|
|
100
|
+
description: 'Ambiguous worker'
|
|
101
|
+
}
|
|
102
|
+
})).to.throw('exactly one of model or useParent');
|
|
103
|
+
});
|
|
104
|
+
});
|
package/pnpm-workspace.yaml
CHANGED
package/skills/modelmix/SKILL.md
CHANGED
|
@@ -92,6 +92,24 @@ const model = ModelMix.new()
|
|
|
92
92
|
|
|
93
93
|
If `sonnet46` fails, it automatically tries `gpt52`, then `gemini3flash`.
|
|
94
94
|
|
|
95
|
+
### Instance plugins
|
|
96
|
+
|
|
97
|
+
Register middleware with `.use({ name, execute })`. Plugins are scoped to the instance, run in registration order, and receive the rendered provider-neutral request. They may edit `context.request`, call `next()`, or return a complete ModelMix result.
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
model.use({
|
|
101
|
+
name: 'metrics',
|
|
102
|
+
async execute(context, next) {
|
|
103
|
+
const result = await next();
|
|
104
|
+
return { ...result, executionId: context.execution.executionId };
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`context.invoke()` starts a child without conversation history. Its `plugins` policy is `'inherit'`, `'none'`, `{ include: [...] }`, or `{ exclude: [...] }`. Passing `model` routes the child through another ModelMix worker chain while preserving execution-tree metadata. Child invocations may pass `assign` with either `system` or `systemFile`; `systemFile` uses the ordinary ModelMix EJS renderer and relative includes.
|
|
110
|
+
|
|
111
|
+
The optional `@modelmix/rlm` package is a separate workspace/npm package for recursive processing of large structured inputs. Pass Markdown through `documents: { name: { format: 'markdown', content } }`, register named ModelMix worker chains, and provide every runtime limit explicitly. A worker uses either `model: anotherModelMixInstance` or `useParent: true`. Its planner sees content-free variable size/shape metadata, while document values and generated orchestration code stay inside an `isolated-vm` sandbox. RLM planner prompts are Markdown files rendered with the normal child `assign` plus `systemFile` path.
|
|
112
|
+
|
|
95
113
|
### Unified effort
|
|
96
114
|
|
|
97
115
|
Provider-agnostic reasoning intensity. **Not** an `options` field — use `config.effort` or `.effort(n)`.
|
|
@@ -114,7 +132,7 @@ ModelMix.new({ config: { effort: 80 } })
|
|
|
114
132
|
| DeepSeek V4 | off | `low`↑ | `high`↑ | `high`↑ | `max`↑ | — |
|
|
115
133
|
| MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
|
|
116
134
|
|
|
117
|
-
\* Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. Anthropic: adaptive + `output_config.effort` on Claude 5 / Fable / Opus 4.6+ / Sonnet 4.6+; Sonnet 4.5 / Haiku 4.5 use `thinking.type=enabled` + `budget_tokens`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. Former `*think()` methods are removed — use `.effort(n).<model>()`. Kimi: `kimiK25()` / `kimiK26()`. Grok 4.20: `.grok420()` non-reasoning; `.effort(20+|-1).grok420()` selects reasoning.
|
|
135
|
+
\* Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.7 Flash supports only `low` / `medium` / `high`, so the first two bands clamp to `low`; `-1` keeps its native `medium` default. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. Anthropic: adaptive + `output_config.effort` on Claude 5 / Fable / Opus 4.6+ / Sonnet 4.6+; Sonnet 4.5 / Haiku 4.5 use `thinking.type=enabled` + `budget_tokens`. Grok 4.6 maps 0–39 / 40–59 / 60–79 / 80–100 to `low` / `medium` / `high` / `xhigh`; without effort it uses native `high`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. Former `*think()` methods are removed — use `.effort(n).<model>()`. Kimi: `kimiK25()` / `kimiK26()`. Grok 4.20: `.grok420()` non-reasoning; `.effort(20+|-1).grok420()` selects reasoning.
|
|
118
136
|
|
|
119
137
|
## Available Model Shorthands
|
|
120
138
|
|
|
@@ -127,10 +145,10 @@ ModelMix.new({ config: { effort: 80 } })
|
|
|
127
145
|
Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.effort(100).opus50()`. `fable5()` and `opus5()` remain available as compatibility aliases.
|
|
128
146
|
|
|
129
147
|
### Google
|
|
130
|
-
`gemini3pro()` `gemini3flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()` `gemini25pro()` `gemini25flash()`
|
|
148
|
+
`gemini3pro()` `gemini3flash()` `gemini37flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()` `gemini25pro()` `gemini25flash()`
|
|
131
149
|
|
|
132
150
|
### Grok
|
|
133
|
-
`grok45()` `grok43()` `grok420multiAgent()` `grok420()`
|
|
151
|
+
`grok46()` `grok45()` `grok43()` `grok420multiAgent()` `grok420()`
|
|
134
152
|
|
|
135
153
|
### Perplexity
|
|
136
154
|
`sonar()` `sonarPro()`
|
|
@@ -302,7 +320,7 @@ const model = ModelMix.new().gpt5mini().addText("Hello!");
|
|
|
302
320
|
const text = await model.message();
|
|
303
321
|
console.log(model.lastRaw.tokens);
|
|
304
322
|
// {
|
|
305
|
-
// input: 1200, output: 50, total: 1250,
|
|
323
|
+
// input: 1200, output: 50, thinking: 0, total: 1250,
|
|
306
324
|
// cached: 1024, cacheWrite: 0, uncachedInput: 176,
|
|
307
325
|
// cacheWrite5m: 0, cacheWrite1h: 0,
|
|
308
326
|
// cacheHitRate: 0.8533, cacheSavings: 0.00018432,
|
|
@@ -586,6 +604,7 @@ const model = ModelMix.new({
|
|
|
586
604
|
| `.addTools([{tool, callback}])` | `this` | Register multiple tools |
|
|
587
605
|
| `.removeTool(name)` | `this` | Remove a tool |
|
|
588
606
|
| `.listTools()` | `{local, mcp}` | List registered tools |
|
|
607
|
+
| `.use(plugin)` | `this` | Register instance-scoped execution middleware |
|
|
589
608
|
| `.new()` | `ModelMix` | Clone instance sharing models |
|
|
590
609
|
| `.attach(key, provider)` | `this` | Attach custom provider |
|
|
591
610
|
|
package/test/effort.test.js
CHANGED
|
@@ -121,6 +121,19 @@ describe('Unified effort scale', () => {
|
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
it('clamps Gemini 3.7 Flash to low, medium, and high', () => {
|
|
125
|
+
expect(mapEffort('google', 0, 'gemini-3.7-flash')).to.deep.equal({
|
|
126
|
+
thinkingConfig: { thinkingLevel: 'low' }
|
|
127
|
+
});
|
|
128
|
+
expect(mapEffort('google', 50, 'gemini-3.7-flash')).to.deep.equal({
|
|
129
|
+
thinkingConfig: { thinkingLevel: 'medium' }
|
|
130
|
+
});
|
|
131
|
+
expect(mapEffort('google', 100, 'gemini-3.7-flash')).to.deep.equal({
|
|
132
|
+
thinkingConfig: { thinkingLevel: 'high' }
|
|
133
|
+
});
|
|
134
|
+
expect(mapEffort('google', -1, 'gemini-3.7-flash')).to.equal(null);
|
|
135
|
+
});
|
|
136
|
+
|
|
124
137
|
it('clamps Gemini levels for models with fewer steps', () => {
|
|
125
138
|
expect(mapEffort('google', 10, 'gemini-3-pro-preview')).to.deep.equal({
|
|
126
139
|
thinkingConfig: { thinkingLevel: 'low' }
|
|
@@ -384,7 +397,7 @@ describe('Unified effort scale', () => {
|
|
|
384
397
|
await google.create({
|
|
385
398
|
config: { system: 'sys' },
|
|
386
399
|
options: {
|
|
387
|
-
model: 'gemini-3.
|
|
400
|
+
model: 'gemini-3.7-flash',
|
|
388
401
|
max_tokens: 100,
|
|
389
402
|
messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }],
|
|
390
403
|
thinkingConfig: { thinkingLevel: 'low' }
|
package/test/grok.test.js
CHANGED
|
@@ -2,6 +2,7 @@ const { expect } = require('chai');
|
|
|
2
2
|
const nock = require('nock');
|
|
3
3
|
const { ModelMix, MixGrok } = require('../index.js');
|
|
4
4
|
const {
|
|
5
|
+
mapEffort,
|
|
5
6
|
resolveGrok420ModelKey,
|
|
6
7
|
GROK420_ALIAS,
|
|
7
8
|
GROK420_REASONING,
|
|
@@ -10,6 +11,7 @@ const {
|
|
|
10
11
|
|
|
11
12
|
describe('Grok Model Registration Tests', () => {
|
|
12
13
|
const grokModels = [
|
|
14
|
+
{ method: 'grok46', key: 'grok-4.6' },
|
|
13
15
|
{ method: 'grok45', key: 'grok-4.5' },
|
|
14
16
|
{ method: 'grok43', key: 'grok-4.3' },
|
|
15
17
|
{ method: 'grok420multiAgent', key: 'grok-4.20-multi-agent-0309' },
|
|
@@ -25,6 +27,78 @@ describe('Grok Model Registration Tests', () => {
|
|
|
25
27
|
expect(model.models[0].key).to.equal(grokModel.key);
|
|
26
28
|
});
|
|
27
29
|
}
|
|
30
|
+
|
|
31
|
+
it('forwards options and config through grok46()', () => {
|
|
32
|
+
const options = { reasoning_effort: 'xhigh' };
|
|
33
|
+
const config = { max_history: 3 };
|
|
34
|
+
const model = ModelMix.new().grok46({ options, config });
|
|
35
|
+
|
|
36
|
+
expect(model.models[0].provider).to.be.instanceOf(MixGrok);
|
|
37
|
+
expect(model.models[0].provider.options).to.deep.equal(options);
|
|
38
|
+
expect(model.models[0].provider.config).to.include(config);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('maps unified effort to Grok 4.6 supported levels', () => {
|
|
42
|
+
expect(mapEffort('openai', 0, 'grok-4.6')).to.deep.equal({ reasoning_effort: 'low' });
|
|
43
|
+
expect(mapEffort('openai', 39, 'grok-4.6')).to.deep.equal({ reasoning_effort: 'low' });
|
|
44
|
+
expect(mapEffort('openai', 40, 'grok-4.6')).to.deep.equal({ reasoning_effort: 'medium' });
|
|
45
|
+
expect(mapEffort('openai', 60, 'grok-4.6')).to.deep.equal({ reasoning_effort: 'high' });
|
|
46
|
+
expect(mapEffort('openai', 100, 'grok-4.6')).to.deep.equal({ reasoning_effort: 'xhigh' });
|
|
47
|
+
expect(mapEffort('openai', -1, 'grok-4.6')).to.equal(null);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('sends a supported Grok 4.6 reasoning effort', async () => {
|
|
51
|
+
const originalApiKey = process.env.XAI_API_KEY;
|
|
52
|
+
process.env.XAI_API_KEY = 'test-key';
|
|
53
|
+
const api = nock('https://api.x.ai')
|
|
54
|
+
.post('/v1/chat/completions', body => {
|
|
55
|
+
expect(body.model).to.equal('grok-4.6');
|
|
56
|
+
expect(body.reasoning_effort).to.equal('low');
|
|
57
|
+
return true;
|
|
58
|
+
})
|
|
59
|
+
.reply(200, {
|
|
60
|
+
choices: [{ message: { content: 'ok' } }],
|
|
61
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const response = await ModelMix.new()
|
|
66
|
+
.effort(0)
|
|
67
|
+
.grok46({ config: { apiKey: 'test-key' } })
|
|
68
|
+
.addText('Hi')
|
|
69
|
+
.message();
|
|
70
|
+
|
|
71
|
+
expect(response).to.equal('ok');
|
|
72
|
+
api.done();
|
|
73
|
+
} finally {
|
|
74
|
+
if (originalApiKey === undefined) delete process.env.XAI_API_KEY;
|
|
75
|
+
else process.env.XAI_API_KEY = originalApiKey;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('calculates Grok 4.6 cache and long-context costs', () => {
|
|
80
|
+
expect(ModelMix.calculateCostBreakdown('grok-4.6', {
|
|
81
|
+
input: 1_000_000,
|
|
82
|
+
output: 1_000_000,
|
|
83
|
+
cached: 1_000_000
|
|
84
|
+
})).to.deep.equal({
|
|
85
|
+
uncachedInput: 0,
|
|
86
|
+
cachedInput: 1,
|
|
87
|
+
cacheWrite: 0,
|
|
88
|
+
cacheWrite5m: 0,
|
|
89
|
+
cacheWrite1h: 0,
|
|
90
|
+
output: 12,
|
|
91
|
+
total: 13
|
|
92
|
+
});
|
|
93
|
+
expect(ModelMix.calculateCost('grok-4.6', {
|
|
94
|
+
input: 199_999,
|
|
95
|
+
output: 1_000_000
|
|
96
|
+
})).to.equal(6.399998);
|
|
97
|
+
expect(ModelMix.calculateCost('grok-4.6', {
|
|
98
|
+
input: 200_000,
|
|
99
|
+
output: 1_000_000
|
|
100
|
+
})).to.equal(12.8);
|
|
101
|
+
});
|
|
28
102
|
});
|
|
29
103
|
|
|
30
104
|
describe('Grok 4.20 effort → model resolution', () => {
|
package/test/live.mcp.js
CHANGED
|
@@ -181,8 +181,8 @@ describe('Live MCP Integration Tests', function () {
|
|
|
181
181
|
}
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it('should use custom MCP tools with Gemini 3 Flash', async function () {
|
|
185
|
-
const model = ModelMix.new(setup).
|
|
184
|
+
it('should use custom MCP tools with Gemini 3.7 Flash', async function () {
|
|
185
|
+
const model = ModelMix.new(setup).gemini37flash();
|
|
186
186
|
|
|
187
187
|
// Add password generator tool
|
|
188
188
|
model.addTool({
|
|
@@ -220,7 +220,7 @@ describe('Live MCP Integration Tests', function () {
|
|
|
220
220
|
model.addText('Generate a secure password of 16 characters with symbols.');
|
|
221
221
|
|
|
222
222
|
const response = await model.message();
|
|
223
|
-
console.log(`Gemini 3 Flash with MCP tools: ${response}`);
|
|
223
|
+
console.log(`Gemini 3.7 Flash with MCP tools: ${response}`);
|
|
224
224
|
|
|
225
225
|
expect(response).to.be.a('string');
|
|
226
226
|
// Check password is mentioned and a generated password string is present
|
|
@@ -233,8 +233,8 @@ describe('Live MCP Integration Tests', function () {
|
|
|
233
233
|
|
|
234
234
|
describe('Advanced MCP Tool Integration', function () {
|
|
235
235
|
|
|
236
|
-
it('should use multiple MCP tools with Grok 4.
|
|
237
|
-
const model = ModelMix.new(setup).
|
|
236
|
+
it('should use multiple MCP tools with Grok 4.6', async function () {
|
|
237
|
+
const model = ModelMix.new(setup).grok46();
|
|
238
238
|
|
|
239
239
|
// Add multiple tools
|
|
240
240
|
model.addTools([
|
|
@@ -407,8 +407,8 @@ describe('Live MCP Integration Tests', function () {
|
|
|
407
407
|
expect(result.factorial_result).to.equal(120);
|
|
408
408
|
});
|
|
409
409
|
|
|
410
|
-
it('should use MCP tools with JSON output using Gemini 3 Flash', async function () {
|
|
411
|
-
const model = ModelMix.new(setup).
|
|
410
|
+
it('should use MCP tools with JSON output using Gemini 3.7 Flash', async function () {
|
|
411
|
+
const model = ModelMix.new(setup).gemini37flash();
|
|
412
412
|
|
|
413
413
|
// Add system info tool
|
|
414
414
|
model.addTool({
|
|
@@ -447,7 +447,7 @@ describe('Live MCP Integration Tests', function () {
|
|
|
447
447
|
generated_at: ""
|
|
448
448
|
});
|
|
449
449
|
|
|
450
|
-
console.log(`Gemini 3 Flash with MCP tools JSON result:`, result);
|
|
450
|
+
console.log(`Gemini 3.7 Flash with MCP tools JSON result:`, result);
|
|
451
451
|
|
|
452
452
|
expect(result).to.be.an('object');
|
|
453
453
|
expect(result.timestamp).to.be.a('number');
|
package/test/live.test.js
CHANGED
|
@@ -59,7 +59,7 @@ describe('Live Integration Tests', function () {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it('should process images with Google Gemini', async function () {
|
|
62
|
-
const model = ModelMix.new(setup).
|
|
62
|
+
const model = ModelMix.new(setup).gemini37flash();
|
|
63
63
|
|
|
64
64
|
model.addImageFromUrl(blueSquareBase64)
|
|
65
65
|
.addText('What color is this image? Answer in one word only.');
|
|
@@ -120,7 +120,7 @@ describe('Live Integration Tests', function () {
|
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
it('should return structured JSON with Google Gemini', async function () {
|
|
123
|
-
const model = ModelMix.new(setup).
|
|
123
|
+
const model = ModelMix.new(setup).gemini37flash();
|
|
124
124
|
|
|
125
125
|
model.addText('Generate information about a fictional city.');
|
|
126
126
|
|
|
@@ -193,7 +193,7 @@ describe('Live Integration Tests', function () {
|
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
const grokSeriesTests = [
|
|
196
|
-
{ name: 'Grok 4.
|
|
196
|
+
{ name: 'Grok 4.6', factory: (m) => m.grok46(), token: 'grok46' },
|
|
197
197
|
{ name: 'Grok 4.20 reasoning', factory: (m) => m.effort(50).grok420(), token: 'grok420' },
|
|
198
198
|
{ name: 'Grok 4.20 non-reasoning', factory: (m) => m.grok420(), token: 'grok420nr' }
|
|
199
199
|
];
|
|
@@ -216,8 +216,8 @@ describe('Live Integration Tests', function () {
|
|
|
216
216
|
|
|
217
217
|
describe('Image Processing with JSON Output', function () {
|
|
218
218
|
|
|
219
|
-
it('should process images and return JSON with Grok 4.
|
|
220
|
-
const model = ModelMix.new(setup).
|
|
219
|
+
it('should process images and return JSON with Grok 4.6', async function () {
|
|
220
|
+
const model = ModelMix.new(setup).grok46();
|
|
221
221
|
|
|
222
222
|
model.addImageFromUrl(blueSquareBase64)
|
|
223
223
|
.addText('Analyze this image and provide details in JSON format.');
|
|
@@ -228,7 +228,7 @@ describe('Live Integration Tests', function () {
|
|
|
228
228
|
description: "string"
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
console.log(`Grok 4.
|
|
231
|
+
console.log(`Grok 4.6 image JSON result:`, result);
|
|
232
232
|
|
|
233
233
|
expect(result).to.be.an('object');
|
|
234
234
|
expect(result).to.have.property('color');
|
|
@@ -263,8 +263,8 @@ describe('Live Integration Tests', function () {
|
|
|
263
263
|
expect(result.features).to.be.an('array');
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
it('should return structured JSON with Grok 4.
|
|
267
|
-
const model = ModelMix.new(setup).
|
|
266
|
+
it('should return structured JSON with Grok 4.6', async function () {
|
|
267
|
+
const model = ModelMix.new(setup).grok46();
|
|
268
268
|
|
|
269
269
|
model.addText('Generate information about a fictional technology.');
|
|
270
270
|
|
|
@@ -275,7 +275,7 @@ describe('Live Integration Tests', function () {
|
|
|
275
275
|
power: "1000 qubits"
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
-
console.log(`Grok 4.
|
|
278
|
+
console.log(`Grok 4.6 JSON result:`, result);
|
|
279
279
|
|
|
280
280
|
expect(result).to.be.an('object');
|
|
281
281
|
expect(result).to.have.property('name');
|