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.
Files changed (40) hide show
  1. package/README.md +111 -8
  2. package/RLM_PLUGIN_SPEC.md +465 -0
  3. package/demo/gemini.js +3 -4
  4. package/demo/grok.js +2 -2
  5. package/demo/images.js +2 -2
  6. package/demo/short.js +3 -3
  7. package/effort.js +3 -0
  8. package/index.d.ts +62 -1
  9. package/index.js +355 -49
  10. package/package.json +7 -4
  11. package/plugins/rlm/index.d.ts +194 -0
  12. package/plugins/rlm/index.js +25 -0
  13. package/plugins/rlm/lib/budget.js +153 -0
  14. package/plugins/rlm/lib/isolated-vm-sandbox.js +90 -0
  15. package/plugins/rlm/lib/markdown.js +156 -0
  16. package/plugins/rlm/lib/planner-prompt.js +137 -0
  17. package/plugins/rlm/lib/plugin.js +203 -0
  18. package/plugins/rlm/lib/runtime.js +146 -0
  19. package/plugins/rlm/lib/variable-descriptors.js +228 -0
  20. package/plugins/rlm/lib/worker-catalog.js +70 -0
  21. package/plugins/rlm/package.json +32 -0
  22. package/plugins/rlm/prompts/partials/processing-rules.md +8 -0
  23. package/plugins/rlm/prompts/planner.md +53 -0
  24. package/plugins/rlm/test/budget.test.js +86 -0
  25. package/plugins/rlm/test/fixtures/book.md +24 -0
  26. package/plugins/rlm/test/isolated-vm-sandbox.test.js +114 -0
  27. package/plugins/rlm/test/markdown.test.js +64 -0
  28. package/plugins/rlm/test/planner-template.test.js +140 -0
  29. package/plugins/rlm/test/plugin-contract.test.js +182 -0
  30. package/plugins/rlm/test/rlm-e2e.test.js +338 -0
  31. package/plugins/rlm/test/variable-descriptors.test.js +170 -0
  32. package/plugins/rlm/test/worker-catalog.test.js +104 -0
  33. package/pnpm-workspace.yaml +6 -0
  34. package/skills/modelmix/SKILL.md +23 -4
  35. package/test/effort.test.js +14 -1
  36. package/test/grok.test.js +74 -0
  37. package/test/live.mcp.js +8 -8
  38. package/test/live.test.js +9 -9
  39. package/test/plugins.test.js +356 -0
  40. 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
+ });
@@ -1,3 +1,9 @@
1
+ packages:
2
+ - plugins/*
3
+
4
+ allowBuilds:
5
+ isolated-vm: true
6
+
1
7
  minimumReleaseAgeExclude:
2
8
  - ws@8.21.0
3
9
  - hono@4.12.25
@@ -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
 
@@ -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.6-flash',
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).gemini3flash();
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.3', async function () {
237
- const model = ModelMix.new(setup).grok43();
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).gemini3flash();
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).gemini3flash();
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).gemini3flash();
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.3', factory: (m) => m.grok43(), token: 'grok43' },
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.3', async function () {
220
- const model = ModelMix.new(setup).grok43();
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.3 image JSON result:`, result);
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.3', async function () {
267
- const model = ModelMix.new(setup).grok43();
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.3 JSON result:`, result);
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');