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,356 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { MixCustom, ModelMix } = require('../index.js');
|
|
4
|
+
|
|
5
|
+
function createProvider(handler = async () => ({ message: 'provider', toolCalls: [] })) {
|
|
6
|
+
const provider = new MixCustom();
|
|
7
|
+
provider.create = handler;
|
|
8
|
+
return provider;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('ModelMix plugins', () => {
|
|
12
|
+
it('keeps registration instance-scoped and lets new instances inherit plugins without history', () => {
|
|
13
|
+
const plugin = { name: 'metrics', execute: (_context, next) => next() };
|
|
14
|
+
const parent = ModelMix.new().use(plugin).addText('parent history');
|
|
15
|
+
const sibling = ModelMix.new();
|
|
16
|
+
const child = parent.new();
|
|
17
|
+
|
|
18
|
+
expect(parent.plugins).to.deep.equal([plugin]);
|
|
19
|
+
expect(child.plugins).to.deep.equal([plugin]);
|
|
20
|
+
expect(child.plugins).to.not.equal(parent.plugins);
|
|
21
|
+
expect(child.messages).to.deep.equal([]);
|
|
22
|
+
expect(sibling.plugins).to.deep.equal([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('runs middleware in registration order and allows request transforms', async () => {
|
|
26
|
+
let providerRequest;
|
|
27
|
+
const provider = createProvider(async request => {
|
|
28
|
+
providerRequest = request;
|
|
29
|
+
return { message: 'done', toolCalls: [] };
|
|
30
|
+
});
|
|
31
|
+
const events = [];
|
|
32
|
+
const model = ModelMix.new()
|
|
33
|
+
.attach('custom', provider)
|
|
34
|
+
.use({
|
|
35
|
+
name: 'outer',
|
|
36
|
+
async execute(context, next) {
|
|
37
|
+
events.push('outer:before');
|
|
38
|
+
context.request.system = 'plugin system';
|
|
39
|
+
context.request.messages[0].content[0].text = 'plugin message';
|
|
40
|
+
context.request.options.temperature = 0.25;
|
|
41
|
+
context.request.config.marker = 'plugin config';
|
|
42
|
+
const result = await next();
|
|
43
|
+
events.push('outer:after');
|
|
44
|
+
return { ...result, wrapped: true };
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.use({
|
|
48
|
+
name: 'inner',
|
|
49
|
+
async execute(_context, next) {
|
|
50
|
+
events.push('inner:before');
|
|
51
|
+
const result = await next();
|
|
52
|
+
events.push('inner:after');
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.addText('original');
|
|
57
|
+
|
|
58
|
+
const result = await model.raw();
|
|
59
|
+
|
|
60
|
+
expect(events).to.deep.equal([
|
|
61
|
+
'outer:before',
|
|
62
|
+
'inner:before',
|
|
63
|
+
'inner:after',
|
|
64
|
+
'outer:after'
|
|
65
|
+
]);
|
|
66
|
+
expect(providerRequest.options.messages[0].content[0].text).to.equal('plugin message');
|
|
67
|
+
expect(providerRequest.options.temperature).to.equal(0.25);
|
|
68
|
+
expect(providerRequest.config.system).to.equal('plugin system');
|
|
69
|
+
expect(providerRequest.config.marker).to.equal('plugin config');
|
|
70
|
+
expect(result).to.include({ message: 'done', wrapped: true });
|
|
71
|
+
expect(model.lastRaw).to.equal(result);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('supports short-circuit results across non-streaming output modes', async () => {
|
|
75
|
+
let providerCalls = 0;
|
|
76
|
+
const createModel = () => ModelMix.new()
|
|
77
|
+
.attach('custom', createProvider(async () => {
|
|
78
|
+
providerCalls += 1;
|
|
79
|
+
return { message: 'provider', toolCalls: [] };
|
|
80
|
+
}))
|
|
81
|
+
.use({
|
|
82
|
+
name: 'short-circuit',
|
|
83
|
+
async execute() {
|
|
84
|
+
return { message: '```json\n{"answer":"plugin"}\n```', source: 'plugin' };
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.addText('ignored');
|
|
88
|
+
|
|
89
|
+
expect(await createModel().message()).to.include('"answer":"plugin"');
|
|
90
|
+
expect(await createModel().block()).to.equal('{"answer":"plugin"}');
|
|
91
|
+
expect(await createModel().json()).to.deep.equal({ answer: 'plugin' });
|
|
92
|
+
expect(await createModel().raw()).to.include({ source: 'plugin' });
|
|
93
|
+
expect(providerCalls).to.equal(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('wraps a complete tool-call execution without rerunning middleware', async () => {
|
|
97
|
+
const providerMessages = [];
|
|
98
|
+
let providerCalls = 0;
|
|
99
|
+
let middlewareCalls = 0;
|
|
100
|
+
const provider = createProvider(async ({ options }) => {
|
|
101
|
+
providerCalls += 1;
|
|
102
|
+
providerMessages.push(options.messages);
|
|
103
|
+
if (providerCalls === 1) {
|
|
104
|
+
return {
|
|
105
|
+
message: '',
|
|
106
|
+
toolCalls: [{ id: 'tool-1', name: 'noop', input: {} }]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { message: 'tool complete', toolCalls: [] };
|
|
110
|
+
});
|
|
111
|
+
const model = ModelMix.new()
|
|
112
|
+
.attach('custom', provider)
|
|
113
|
+
.addTool({
|
|
114
|
+
name: 'noop',
|
|
115
|
+
description: 'Return a fixed result.',
|
|
116
|
+
inputSchema: { type: 'object' }
|
|
117
|
+
}, async () => 'ok')
|
|
118
|
+
.use({
|
|
119
|
+
name: 'transform',
|
|
120
|
+
async execute(context, next) {
|
|
121
|
+
middlewareCalls += 1;
|
|
122
|
+
context.request.messages[0].content[0].text = 'transformed prompt';
|
|
123
|
+
return next();
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
.addText('original prompt');
|
|
127
|
+
|
|
128
|
+
expect(await model.message()).to.equal('tool complete');
|
|
129
|
+
expect(middlewareCalls).to.equal(1);
|
|
130
|
+
expect(providerCalls).to.equal(2);
|
|
131
|
+
expect(providerMessages[0][0].content[0].text).to.equal('transformed prompt');
|
|
132
|
+
expect(providerMessages[1][0].content[0].text).to.equal('transformed prompt');
|
|
133
|
+
expect(providerMessages[1].some(message => message.role === 'tool')).to.equal(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
for (const [policyName, policy, expectedPlugins] of [
|
|
137
|
+
['inherit', 'inherit', ['parent', 'alpha', 'beta']],
|
|
138
|
+
['none', 'none', []],
|
|
139
|
+
['include', { include: ['beta'] }, ['beta']],
|
|
140
|
+
['exclude', { exclude: ['parent', 'alpha'] }, ['beta']]
|
|
141
|
+
]) {
|
|
142
|
+
it(`applies the ${policyName} child plugin policy and execution metadata`, async () => {
|
|
143
|
+
const events = [];
|
|
144
|
+
let childProviderRequest;
|
|
145
|
+
const provider = createProvider(async request => {
|
|
146
|
+
childProviderRequest = request;
|
|
147
|
+
return { message: 'child result', toolCalls: [] };
|
|
148
|
+
});
|
|
149
|
+
const parentPlugin = {
|
|
150
|
+
name: 'parent',
|
|
151
|
+
async execute(context, next) {
|
|
152
|
+
events.push({ name: 'parent', ...context.execution });
|
|
153
|
+
if (context.execution.depth > 0) return next();
|
|
154
|
+
return context.invoke({
|
|
155
|
+
system: 'child system',
|
|
156
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: 'child prompt' }] }],
|
|
157
|
+
plugins: policy
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const recorder = name => ({
|
|
162
|
+
name,
|
|
163
|
+
async execute(context, next) {
|
|
164
|
+
events.push({ name, ...context.execution });
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const model = ModelMix.new({ config: { max_history: -1 } })
|
|
169
|
+
.attach('custom', provider)
|
|
170
|
+
.use(parentPlugin)
|
|
171
|
+
.use(recorder('alpha'))
|
|
172
|
+
.use(recorder('beta'))
|
|
173
|
+
.addText('parent prompt');
|
|
174
|
+
|
|
175
|
+
const result = await model.raw();
|
|
176
|
+
const childEvents = events.filter(event => event.depth === 1);
|
|
177
|
+
|
|
178
|
+
expect(result.message).to.equal('child result');
|
|
179
|
+
expect(result.execution).to.deep.include({
|
|
180
|
+
parentExecutionId: events[0].executionId,
|
|
181
|
+
depth: 1
|
|
182
|
+
});
|
|
183
|
+
expect(childEvents.map(event => event.name)).to.deep.equal(expectedPlugins);
|
|
184
|
+
expect(events[0].depth).to.equal(0);
|
|
185
|
+
expect(events[0].parentExecutionId).to.equal(null);
|
|
186
|
+
for (const childEvent of childEvents) {
|
|
187
|
+
expect(childEvent.executionId).to.equal(childEvents[0].executionId);
|
|
188
|
+
expect(childEvent.parentExecutionId).to.equal(events[0].executionId);
|
|
189
|
+
}
|
|
190
|
+
expect(childEvents[0]?.executionId).to.not.equal(events[0].executionId);
|
|
191
|
+
expect(childProviderRequest.options.messages).to.deep.equal([
|
|
192
|
+
{ role: 'user', content: [{ type: 'text', text: 'child prompt' }] }
|
|
193
|
+
]);
|
|
194
|
+
expect(childProviderRequest.config.system).to.equal('child system');
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
it('can invoke a child through another ModelMix worker chain', async () => {
|
|
199
|
+
let parentProviderCalls = 0;
|
|
200
|
+
const parentProvider = createProvider(async () => {
|
|
201
|
+
parentProviderCalls += 1;
|
|
202
|
+
return { message: 'parent', toolCalls: [] };
|
|
203
|
+
});
|
|
204
|
+
let workerRequest;
|
|
205
|
+
const worker = ModelMix.new({
|
|
206
|
+
options: { temperature: 0.25 },
|
|
207
|
+
config: { workerPolicy: 'preserved' }
|
|
208
|
+
}).attach('worker', createProvider(async request => {
|
|
209
|
+
workerRequest = request;
|
|
210
|
+
return {
|
|
211
|
+
message: 'worker result',
|
|
212
|
+
toolCalls: []
|
|
213
|
+
};
|
|
214
|
+
}));
|
|
215
|
+
const model = ModelMix.new()
|
|
216
|
+
.attach('parent', parentProvider)
|
|
217
|
+
.use({
|
|
218
|
+
name: 'worker-router',
|
|
219
|
+
execute(context) {
|
|
220
|
+
return context.invoke({
|
|
221
|
+
model: worker,
|
|
222
|
+
messages: [{ role: 'user', content: 'worker prompt' }],
|
|
223
|
+
plugins: 'none'
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
.addText('parent prompt');
|
|
228
|
+
|
|
229
|
+
expect(await model.message()).to.equal('worker result');
|
|
230
|
+
expect(parentProviderCalls).to.equal(0);
|
|
231
|
+
expect(workerRequest.options.temperature).to.equal(0.25);
|
|
232
|
+
expect(workerRequest.config.workerPolicy).to.equal('preserved');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('renders child system files with ModelMix assign data and relative includes', async () => {
|
|
236
|
+
let providerRequest;
|
|
237
|
+
const model = ModelMix.new()
|
|
238
|
+
.attach('custom', createProvider(async request => {
|
|
239
|
+
providerRequest = request;
|
|
240
|
+
return { message: 'rendered', toolCalls: [] };
|
|
241
|
+
}))
|
|
242
|
+
.use({
|
|
243
|
+
name: 'template-child',
|
|
244
|
+
execute(context) {
|
|
245
|
+
return context.invoke({
|
|
246
|
+
systemFile: path.join(__dirname, 'fixtures/system-template.txt'),
|
|
247
|
+
assign: {
|
|
248
|
+
role: '<%- mustRemainData %>',
|
|
249
|
+
language: 'English'
|
|
250
|
+
},
|
|
251
|
+
messages: [{ role: 'user', content: 'child task' }],
|
|
252
|
+
plugins: 'none'
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
.addText('parent task');
|
|
257
|
+
|
|
258
|
+
expect(await model.message()).to.equal('rendered');
|
|
259
|
+
expect(providerRequest.config.system.trimEnd()).to.equal([
|
|
260
|
+
'You are a <%- mustRemainData %>.',
|
|
261
|
+
'Always respond in English.'
|
|
262
|
+
].join('\n'));
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('rejects duplicate names, invalid child history, and repeated next calls', async () => {
|
|
266
|
+
const duplicate = { name: 'duplicate', execute: (_context, next) => next() };
|
|
267
|
+
const model = ModelMix.new().use(duplicate);
|
|
268
|
+
expect(() => model.use(duplicate)).to.throw('already registered');
|
|
269
|
+
|
|
270
|
+
const historyModel = ModelMix.new()
|
|
271
|
+
.attach('custom', createProvider())
|
|
272
|
+
.use({
|
|
273
|
+
name: 'history',
|
|
274
|
+
execute(context) {
|
|
275
|
+
return context.invoke({ messages: [], history: true });
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
.addText('parent');
|
|
279
|
+
let historyError;
|
|
280
|
+
try {
|
|
281
|
+
await historyModel.raw();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
historyError = error;
|
|
284
|
+
}
|
|
285
|
+
expect(historyError).to.be.instanceOf(TypeError);
|
|
286
|
+
expect(historyError.message).to.include('history: false');
|
|
287
|
+
|
|
288
|
+
const nextModel = ModelMix.new()
|
|
289
|
+
.attach('custom', createProvider())
|
|
290
|
+
.use({
|
|
291
|
+
name: 'twice',
|
|
292
|
+
async execute(_context, next) {
|
|
293
|
+
await next();
|
|
294
|
+
return next();
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
.addText('parent');
|
|
298
|
+
let nextError;
|
|
299
|
+
try {
|
|
300
|
+
await nextModel.raw();
|
|
301
|
+
} catch (error) {
|
|
302
|
+
nextError = error;
|
|
303
|
+
}
|
|
304
|
+
expect(nextError).to.be.instanceOf(Error);
|
|
305
|
+
expect(nextError.message).to.include('multiple times');
|
|
306
|
+
|
|
307
|
+
const conflictingSystemModel = ModelMix.new()
|
|
308
|
+
.attach('custom', createProvider())
|
|
309
|
+
.use({
|
|
310
|
+
name: 'conflicting-system',
|
|
311
|
+
execute(context) {
|
|
312
|
+
return context.invoke({
|
|
313
|
+
system: 'inline',
|
|
314
|
+
systemFile: 'system.md',
|
|
315
|
+
messages: [],
|
|
316
|
+
plugins: 'none'
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
.addText('parent');
|
|
321
|
+
let conflictingSystemError;
|
|
322
|
+
try {
|
|
323
|
+
await conflictingSystemModel.raw();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
conflictingSystemError = error;
|
|
326
|
+
}
|
|
327
|
+
expect(conflictingSystemError).to.be.instanceOf(TypeError);
|
|
328
|
+
expect(conflictingSystemError.message).to.include('only one of system or systemFile');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('does not fall through to providers when a plugin fails', async () => {
|
|
332
|
+
let providerCalls = 0;
|
|
333
|
+
const model = ModelMix.new()
|
|
334
|
+
.attach('custom', createProvider(async () => {
|
|
335
|
+
providerCalls += 1;
|
|
336
|
+
return { message: 'provider', toolCalls: [] };
|
|
337
|
+
}))
|
|
338
|
+
.use({
|
|
339
|
+
name: 'failure',
|
|
340
|
+
async execute() {
|
|
341
|
+
throw new Error('plugin failure');
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
.addText('prompt');
|
|
345
|
+
|
|
346
|
+
let failure;
|
|
347
|
+
try {
|
|
348
|
+
await model.raw();
|
|
349
|
+
} catch (error) {
|
|
350
|
+
failure = error;
|
|
351
|
+
}
|
|
352
|
+
expect(failure).to.be.instanceOf(Error);
|
|
353
|
+
expect(failure.message).to.equal('plugin failure');
|
|
354
|
+
expect(providerCalls).to.equal(0);
|
|
355
|
+
});
|
|
356
|
+
});
|
package/test/tokens.test.js
CHANGED
|
@@ -53,7 +53,8 @@ describe('Token Usage Tracking', () => {
|
|
|
53
53
|
usageMetadata: {
|
|
54
54
|
promptTokenCount: 70,
|
|
55
55
|
candidatesTokenCount: 10,
|
|
56
|
-
|
|
56
|
+
thoughtsTokenCount: 5,
|
|
57
|
+
totalTokenCount: 85,
|
|
57
58
|
cachedContentTokenCount: 35
|
|
58
59
|
}
|
|
59
60
|
});
|
|
@@ -88,7 +89,8 @@ describe('Token Usage Tracking', () => {
|
|
|
88
89
|
expect(googleTokens).to.include({
|
|
89
90
|
input: 70,
|
|
90
91
|
output: 10,
|
|
91
|
-
|
|
92
|
+
thinking: 5,
|
|
93
|
+
total: 85,
|
|
92
94
|
cached: 35,
|
|
93
95
|
cacheWrite: 0,
|
|
94
96
|
uncachedInput: 35,
|
|
@@ -500,21 +502,51 @@ describe('Token Usage Tracking', () => {
|
|
|
500
502
|
|
|
501
503
|
it('should register Gemini Flash shortcuts with Google provider', function () {
|
|
502
504
|
const model = ModelMix.new()
|
|
505
|
+
.gemini37flash()
|
|
503
506
|
.gemini36flash()
|
|
504
507
|
.gemini35flash()
|
|
505
508
|
.gemini35flashLite();
|
|
506
509
|
|
|
507
510
|
expect(model.models.map(({ key }) => key)).to.deep.equal([
|
|
511
|
+
'gemini-3.7-flash',
|
|
508
512
|
'gemini-3.6-flash',
|
|
509
513
|
'gemini-3.5-flash',
|
|
510
514
|
'gemini-3.5-flash-lite'
|
|
511
515
|
]);
|
|
512
516
|
expect(model.models.every(({ provider }) => provider instanceof MixGoogle)).to.equal(true);
|
|
513
|
-
expect(ModelMix.calculateCost('gemini-3.
|
|
517
|
+
expect(ModelMix.calculateCost('gemini-3.7-flash', { input: 1_000_000, output: 1_000_000 })).to.equal(4.5);
|
|
518
|
+
expect(ModelMix.calculateCost('gemini-3.6-flash', { input: 1_000_000, output: 1_000_000 })).to.equal(4.5);
|
|
514
519
|
expect(ModelMix.calculateCost('gemini-3.5-flash', { input: 1_000_000, output: 1_000_000 })).to.equal(5.25);
|
|
515
520
|
expect(ModelMix.calculateCost('gemini-3.5-flash-lite', { input: 1_000_000, output: 1_000_000 })).to.equal(2.8);
|
|
516
521
|
});
|
|
517
522
|
|
|
523
|
+
it('should calculate Gemini 3.7 Flash cache reads at the introductory rate', function () {
|
|
524
|
+
expect(ModelMix.calculateCostBreakdown('gemini-3.7-flash', {
|
|
525
|
+
input: 1_000_000,
|
|
526
|
+
output: 1_000_000,
|
|
527
|
+
thinking: 500_000,
|
|
528
|
+
cached: 1_000_000
|
|
529
|
+
})).to.deep.equal({
|
|
530
|
+
uncachedInput: 0,
|
|
531
|
+
cachedInput: 0.075,
|
|
532
|
+
cacheWrite: 0,
|
|
533
|
+
cacheWrite5m: 0,
|
|
534
|
+
cacheWrite1h: 0,
|
|
535
|
+
output: 5.625,
|
|
536
|
+
total: 5.7
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('should forward options and config through gemini37flash()', function () {
|
|
541
|
+
const options = { thinkingLevel: 'high' };
|
|
542
|
+
const config = { max_history: 3 };
|
|
543
|
+
const model = ModelMix.new().gemini37flash({ options, config });
|
|
544
|
+
|
|
545
|
+
expect(model.models[0].provider).to.be.instanceOf(MixGoogle);
|
|
546
|
+
expect(model.models[0].provider.options).to.deep.equal(options);
|
|
547
|
+
expect(model.models[0].provider.config).to.include(config);
|
|
548
|
+
});
|
|
549
|
+
|
|
518
550
|
it('should register MiMo shortcuts with native and OpenRouter providers', function () {
|
|
519
551
|
const originalMimoApiKey = process.env.MIMO_API_KEY;
|
|
520
552
|
const originalOpenRouterApiKey = process.env.OPENROUTER_API_KEY;
|
|
@@ -620,7 +652,7 @@ describe('Token Usage Tracking', () => {
|
|
|
620
652
|
this.timeout(30000);
|
|
621
653
|
|
|
622
654
|
const model = ModelMix.new()
|
|
623
|
-
.
|
|
655
|
+
.gemini37flash()
|
|
624
656
|
.addText('Say hi');
|
|
625
657
|
|
|
626
658
|
const result = await model.raw();
|
|
@@ -683,7 +715,7 @@ describe('Token Usage Tracking', () => {
|
|
|
683
715
|
const providers = [
|
|
684
716
|
{ name: 'OpenAI', create: (m) => m.gpt56luna() },
|
|
685
717
|
{ name: 'Anthropic', create: (m) => m.haiku45() },
|
|
686
|
-
{ name: 'Google', create: (m) => m.
|
|
718
|
+
{ name: 'Google', create: (m) => m.gemini37flash() }
|
|
687
719
|
];
|
|
688
720
|
|
|
689
721
|
for (const provider of providers) {
|