modelmix 4.7.2 → 5.0.0
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 +210 -88
- package/demo/demo.js +4 -4
- package/demo/prompt.md +2 -2
- package/http-client.js +15 -2
- package/index.d.ts +101 -11
- package/index.js +899 -272
- package/package.json +4 -1
- package/skills/modelmix/SKILL.md +76 -19
- package/test/README.md +10 -9
- package/test/anthropic.test.js +146 -9
- package/test/effort.test.js +2 -2
- package/test/fallback.test.js +192 -2
- package/test/fixtures/account-details.txt +4 -0
- package/test/fixtures/choice-options.txt +6 -0
- package/test/fixtures/choice-template.txt +2 -0
- package/test/fixtures/system-rules.txt +1 -0
- package/test/fixtures/system-template.txt +2 -0
- package/test/fixtures/template.txt +4 -11
- package/test/grok.test.js +28 -1
- package/test/history.test.js +2 -2
- package/test/live.mcp.js +20 -20
- package/test/live.test.js +11 -11
- package/test/templates.test.js +471 -281
- package/test/tokens.test.js +414 -18
package/test/tokens.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect } from 'chai';
|
|
2
|
-
import { ModelMix, MixAnthropic, MixCustom, MixGoogle, MixMiMo, MixOpenAIResponses, MixOpenRouter } from '../index.js';
|
|
2
|
+
import { ModelMix, MixAnthropic, MixCustom, MixGoogle, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenRouter } from '../index.js';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
|
|
5
5
|
const require = createRequire(import.meta.url);
|
|
@@ -18,14 +18,15 @@ describe('Token Usage Tracking', () => {
|
|
|
18
18
|
nock.activate();
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it('should
|
|
21
|
+
it('should normalize prompt cache usage from supported provider formats', function () {
|
|
22
22
|
const openAIChatTokens = MixCustom.extractTokens({
|
|
23
23
|
usage: {
|
|
24
24
|
prompt_tokens: 120,
|
|
25
25
|
completion_tokens: 30,
|
|
26
26
|
total_tokens: 150,
|
|
27
27
|
prompt_tokens_details: {
|
|
28
|
-
cached_tokens: 80
|
|
28
|
+
cached_tokens: 80,
|
|
29
|
+
cache_write_tokens: 10
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
});
|
|
@@ -35,7 +36,8 @@ describe('Token Usage Tracking', () => {
|
|
|
35
36
|
output_tokens: 20,
|
|
36
37
|
total_tokens: 110,
|
|
37
38
|
input_tokens_details: {
|
|
38
|
-
cached_tokens: 45
|
|
39
|
+
cached_tokens: 45,
|
|
40
|
+
cache_write_tokens: 15
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
});
|
|
@@ -43,7 +45,8 @@ describe('Token Usage Tracking', () => {
|
|
|
43
45
|
usage: {
|
|
44
46
|
input_tokens: 60,
|
|
45
47
|
output_tokens: 15,
|
|
46
|
-
cache_read_input_tokens: 25
|
|
48
|
+
cache_read_input_tokens: 25,
|
|
49
|
+
cache_creation_input_tokens: 10
|
|
47
50
|
}
|
|
48
51
|
});
|
|
49
52
|
const googleTokens = MixGoogle.extractTokens({
|
|
@@ -55,10 +58,69 @@ describe('Token Usage Tracking', () => {
|
|
|
55
58
|
}
|
|
56
59
|
});
|
|
57
60
|
|
|
58
|
-
expect(openAIChatTokens
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
expect(openAIChatTokens).to.include({
|
|
62
|
+
input: 120,
|
|
63
|
+
output: 30,
|
|
64
|
+
total: 150,
|
|
65
|
+
cached: 80,
|
|
66
|
+
cacheWrite: 10,
|
|
67
|
+
uncachedInput: 30,
|
|
68
|
+
cacheHitRate: 0.6667
|
|
69
|
+
});
|
|
70
|
+
expect(openAIResponsesTokens).to.include({
|
|
71
|
+
input: 90,
|
|
72
|
+
output: 20,
|
|
73
|
+
total: 110,
|
|
74
|
+
cached: 45,
|
|
75
|
+
cacheWrite: 15,
|
|
76
|
+
uncachedInput: 30,
|
|
77
|
+
cacheHitRate: 0.5
|
|
78
|
+
});
|
|
79
|
+
expect(anthropicTokens).to.include({
|
|
80
|
+
input: 95,
|
|
81
|
+
output: 15,
|
|
82
|
+
total: 110,
|
|
83
|
+
cached: 25,
|
|
84
|
+
cacheWrite: 10,
|
|
85
|
+
uncachedInput: 60,
|
|
86
|
+
cacheHitRate: 0.2632
|
|
87
|
+
});
|
|
88
|
+
expect(googleTokens).to.include({
|
|
89
|
+
input: 70,
|
|
90
|
+
output: 10,
|
|
91
|
+
total: 80,
|
|
92
|
+
cached: 35,
|
|
93
|
+
cacheWrite: 0,
|
|
94
|
+
uncachedInput: 35,
|
|
95
|
+
cacheHitRate: 0.5
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return zero for prompt cache categories omitted by the provider', function () {
|
|
100
|
+
const tokens = MixCustom.extractTokens({
|
|
101
|
+
usage: {
|
|
102
|
+
prompt_tokens: 20,
|
|
103
|
+
completion_tokens: 5,
|
|
104
|
+
total_tokens: 25
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(tokens).to.include({
|
|
109
|
+
cached: 0,
|
|
110
|
+
cacheWrite: 0,
|
|
111
|
+
uncachedInput: 20,
|
|
112
|
+
cacheHitRate: 0,
|
|
113
|
+
cost: 0
|
|
114
|
+
});
|
|
115
|
+
expect(tokens.costBreakdown).to.deep.equal({
|
|
116
|
+
uncachedInput: 0,
|
|
117
|
+
cachedInput: 0,
|
|
118
|
+
cacheWrite: 0,
|
|
119
|
+
cacheWrite5m: 0,
|
|
120
|
+
cacheWrite1h: 0,
|
|
121
|
+
output: 0,
|
|
122
|
+
total: 0
|
|
123
|
+
});
|
|
62
124
|
});
|
|
63
125
|
|
|
64
126
|
it('should pass OpenAI Responses prompt cache options through the request body', function () {
|
|
@@ -76,6 +138,186 @@ describe('Token Usage Tracking', () => {
|
|
|
76
138
|
expect(request.prompt_cache_retention).to.equal('24h');
|
|
77
139
|
});
|
|
78
140
|
|
|
141
|
+
it('should pass GPT-5.6 explicit cache controls and preserve block breakpoints', function () {
|
|
142
|
+
const breakpoint = { mode: 'explicit' };
|
|
143
|
+
const request = MixOpenAIResponses.buildResponsesRequest({
|
|
144
|
+
model: 'gpt-5.6-luna',
|
|
145
|
+
messages: [{
|
|
146
|
+
role: 'developer',
|
|
147
|
+
content: [
|
|
148
|
+
{ type: 'text', text: 'Stable instructions', prompt_cache_breakpoint: breakpoint },
|
|
149
|
+
{
|
|
150
|
+
type: 'image',
|
|
151
|
+
source: { type: 'base64', media_type: 'image/png', data: 'AAAA' },
|
|
152
|
+
prompt_cache_breakpoint: breakpoint
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: 'input_file',
|
|
156
|
+
file_id: 'file_123',
|
|
157
|
+
prompt_cache_breakpoint: breakpoint
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
}],
|
|
161
|
+
prompt_cache_key: 'explicit-cache',
|
|
162
|
+
prompt_cache_options: { mode: 'explicit', ttl: '30m' }
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(request.prompt_cache_options).to.deep.equal({ mode: 'explicit', ttl: '30m' });
|
|
166
|
+
expect(request.input[0].content).to.deep.equal([
|
|
167
|
+
{ type: 'input_text', text: 'Stable instructions', prompt_cache_breakpoint: breakpoint },
|
|
168
|
+
{
|
|
169
|
+
type: 'input_image',
|
|
170
|
+
image_url: 'data:image/png;base64,AAAA',
|
|
171
|
+
prompt_cache_breakpoint: breakpoint
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'input_file',
|
|
175
|
+
file_id: 'file_123',
|
|
176
|
+
prompt_cache_breakpoint: breakpoint
|
|
177
|
+
}
|
|
178
|
+
]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should translate neutral cache breakpoints for GPT-5.6 and filter them for older models', async function () {
|
|
182
|
+
const breakpoint = { mode: 'explicit' };
|
|
183
|
+
const model = ModelMix.new()
|
|
184
|
+
.addText('Stable text', { role: 'developer', cache: { breakpoint: true } })
|
|
185
|
+
.addImageFromUrl('data:image/png;base64,AAAA', { cache: { breakpoint: true } });
|
|
186
|
+
const messages = await model.prepareMessages();
|
|
187
|
+
const gpt56Request = MixOpenAIResponses.buildResponsesRequest({
|
|
188
|
+
model: 'gpt-5.6-luna',
|
|
189
|
+
messages,
|
|
190
|
+
prompt_cache_options: { mode: 'explicit', ttl: '30m' }
|
|
191
|
+
});
|
|
192
|
+
const olderRequest = MixOpenAIResponses.buildResponsesRequest({
|
|
193
|
+
model: 'gpt-5.4',
|
|
194
|
+
messages
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(messages[0].content[0]).to.deep.include({ cache: { breakpoint: true } });
|
|
198
|
+
expect(messages[0].content[0]).to.not.have.property('prompt_cache_breakpoint');
|
|
199
|
+
expect(gpt56Request.input[0].content[0].prompt_cache_breakpoint).to.deep.equal(breakpoint);
|
|
200
|
+
expect(gpt56Request.input[1].content[0]).to.deep.equal({
|
|
201
|
+
type: 'input_image',
|
|
202
|
+
image_url: 'data:image/png;base64,AAAA',
|
|
203
|
+
prompt_cache_breakpoint: breakpoint
|
|
204
|
+
});
|
|
205
|
+
expect(olderRequest.input[0].content[0]).to.not.have.property('cache');
|
|
206
|
+
expect(olderRequest.input[0].content[0]).to.not.have.property('prompt_cache_breakpoint');
|
|
207
|
+
expect(olderRequest.input[1].content[0]).to.not.have.property('prompt_cache_breakpoint');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should map assistant history to Responses output_text content', function () {
|
|
211
|
+
const request = MixOpenAIResponses.buildResponsesRequest({
|
|
212
|
+
model: 'gpt-5.6-luna',
|
|
213
|
+
messages: [
|
|
214
|
+
{ role: 'user', content: [{ type: 'text', text: 'My name is Alice' }] },
|
|
215
|
+
{ role: 'assistant', content: [{ type: 'text', text: 'Nice to meet you' }] },
|
|
216
|
+
{ role: 'user', content: [{ type: 'text', text: 'What is my name?' }] }
|
|
217
|
+
]
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(request.input[0].content[0]).to.deep.equal({
|
|
221
|
+
type: 'input_text',
|
|
222
|
+
text: 'My name is Alice'
|
|
223
|
+
});
|
|
224
|
+
expect(request.input[1].content[0]).to.deep.equal({
|
|
225
|
+
type: 'output_text',
|
|
226
|
+
text: 'Nice to meet you'
|
|
227
|
+
});
|
|
228
|
+
expect(request.input[2].content[0]).to.deep.equal({
|
|
229
|
+
type: 'input_text',
|
|
230
|
+
text: 'What is my name?'
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should not treat the native OpenAI breakpoint as a fluent API alias', async function () {
|
|
235
|
+
const nativeBreakpoint = { prompt_cache_breakpoint: { mode: 'explicit' } };
|
|
236
|
+
const model = ModelMix.new()
|
|
237
|
+
.addText('Stable text', nativeBreakpoint)
|
|
238
|
+
.addImageFromUrl('data:image/png;base64,AAAA', nativeBreakpoint);
|
|
239
|
+
const messages = await model.prepareMessages();
|
|
240
|
+
const request = MixOpenAIResponses.buildResponsesRequest({
|
|
241
|
+
model: 'gpt-5.6-luna',
|
|
242
|
+
messages,
|
|
243
|
+
prompt_cache_options: { mode: 'explicit', ttl: '30m' }
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(messages[0].content).to.have.length(2);
|
|
247
|
+
expect(messages[0].content.every(item => item.cache === undefined)).to.equal(true);
|
|
248
|
+
const content = request.input.flatMap(item => item.content ?? []);
|
|
249
|
+
expect(content).to.have.length(2);
|
|
250
|
+
expect(content.every(item => item.prompt_cache_breakpoint === undefined)).to.equal(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should strip neutral and Anthropic cache metadata from OpenAI-compatible chat requests', function () {
|
|
254
|
+
const messages = [{
|
|
255
|
+
role: 'user',
|
|
256
|
+
content: [{
|
|
257
|
+
type: 'text',
|
|
258
|
+
text: 'Stable text',
|
|
259
|
+
cache: { breakpoint: true },
|
|
260
|
+
cache_control: { type: 'ephemeral' }
|
|
261
|
+
}]
|
|
262
|
+
}];
|
|
263
|
+
const converted = MixOpenAI.convertMessages(messages, { system: 'System' });
|
|
264
|
+
|
|
265
|
+
expect(converted[1].content[0]).to.deep.equal({ type: 'text', text: 'Stable text' });
|
|
266
|
+
expect(messages[0].content[0]).to.deep.include({ cache: { breakpoint: true } });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should reject prompt cache controls unsupported by the selected OpenAI model', function () {
|
|
270
|
+
expect(() => MixOpenAIResponses.buildResponsesRequest({
|
|
271
|
+
model: 'gpt-5.6-luna',
|
|
272
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
273
|
+
prompt_cache_retention: '24h'
|
|
274
|
+
})).to.throw('prompt_cache_options.ttl');
|
|
275
|
+
|
|
276
|
+
expect(() => MixOpenAIResponses.buildResponsesRequest({
|
|
277
|
+
model: 'gpt-5.4',
|
|
278
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
279
|
+
prompt_cache_options: { mode: 'explicit', ttl: '30m' }
|
|
280
|
+
})).to.throw('only supported by GPT-5.6');
|
|
281
|
+
|
|
282
|
+
expect(() => MixOpenAIResponses.buildResponsesRequest({
|
|
283
|
+
model: 'gpt-5.4',
|
|
284
|
+
messages: [{
|
|
285
|
+
role: 'user',
|
|
286
|
+
content: [{
|
|
287
|
+
type: 'text',
|
|
288
|
+
text: 'Hi',
|
|
289
|
+
prompt_cache_breakpoint: { mode: 'explicit' }
|
|
290
|
+
}]
|
|
291
|
+
}]
|
|
292
|
+
})).to.throw('prompt_cache_breakpoint is only supported by GPT-5.6');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should validate GPT-5.6 prompt cache option values', function () {
|
|
296
|
+
expect(() => MixOpenAIResponses.buildResponsesRequest({
|
|
297
|
+
model: 'gpt-5.6-luna',
|
|
298
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
299
|
+
prompt_cache_options: { mode: 'automatic', ttl: '30m' }
|
|
300
|
+
})).to.throw('mode must be "implicit" or "explicit"');
|
|
301
|
+
|
|
302
|
+
expect(() => MixOpenAIResponses.buildResponsesRequest({
|
|
303
|
+
model: 'gpt-5.6-luna',
|
|
304
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
305
|
+
prompt_cache_options: { mode: 'explicit', ttl: '1h' }
|
|
306
|
+
})).to.throw('ttl must be "30m"');
|
|
307
|
+
|
|
308
|
+
expect(() => MixOpenAIResponses.buildResponsesRequest({
|
|
309
|
+
model: 'gpt-5.6-luna',
|
|
310
|
+
messages: [{
|
|
311
|
+
role: 'user',
|
|
312
|
+
content: [{
|
|
313
|
+
type: 'text',
|
|
314
|
+
text: 'Hi',
|
|
315
|
+
prompt_cache_breakpoint: { mode: 'implicit' }
|
|
316
|
+
}]
|
|
317
|
+
}]
|
|
318
|
+
})).to.throw('breakpoint mode must be "explicit"');
|
|
319
|
+
});
|
|
320
|
+
|
|
79
321
|
it('should register GPT-5.5 shortcuts with OpenAI Responses provider', function () {
|
|
80
322
|
const model = ModelMix.new()
|
|
81
323
|
.gpt55()
|
|
@@ -100,23 +342,177 @@ describe('Token Usage Tracking', () => {
|
|
|
100
342
|
'gpt-5.6-luna'
|
|
101
343
|
]);
|
|
102
344
|
expect(model.models.every(({ provider }) => provider instanceof MixOpenAIResponses)).to.equal(true);
|
|
103
|
-
expect(ModelMix.calculateCost('gpt-5.6-sol', { input: 1_000_000, output: 1_000_000 })).to.equal(
|
|
104
|
-
expect(ModelMix.calculateCost('gpt-5.6-terra', { input: 1_000_000, output: 1_000_000 })).to.equal(
|
|
105
|
-
expect(ModelMix.calculateCost('gpt-5.6-luna', { input: 1_000_000, output: 1_000_000 })).to.equal(
|
|
345
|
+
expect(ModelMix.calculateCost('gpt-5.6-sol', { input: 1_000_000, output: 1_000_000 })).to.equal(55);
|
|
346
|
+
expect(ModelMix.calculateCost('gpt-5.6-terra', { input: 1_000_000, output: 1_000_000 })).to.equal(22);
|
|
347
|
+
expect(ModelMix.calculateCost('gpt-5.6-luna', { input: 1_000_000, output: 1_000_000 })).to.equal(2.2);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should calculate GPT-5.6 cache reads and writes at their actual rates', function () {
|
|
351
|
+
const tokens = ModelMix.normalizeTokenUsage({
|
|
352
|
+
input: 1200,
|
|
353
|
+
output: 50,
|
|
354
|
+
cached: 800,
|
|
355
|
+
cacheWrite: 200
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(ModelMix.calculateCostBreakdown('gpt-5.6-terra', tokens)).to.deep.equal({
|
|
359
|
+
uncachedInput: 0.0004,
|
|
360
|
+
cachedInput: 0.00016,
|
|
361
|
+
cacheWrite: 0.0005,
|
|
362
|
+
cacheWrite5m: 0,
|
|
363
|
+
cacheWrite1h: 0,
|
|
364
|
+
output: 0.0006,
|
|
365
|
+
total: 0.00166
|
|
366
|
+
});
|
|
367
|
+
expect(ModelMix.calculateCost('gpt-5.6-terra', tokens)).to.equal(0.00166);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should apply GPT-5.6 long-context multipliers to the entire request', function () {
|
|
371
|
+
const tokens = ModelMix.normalizeTokenUsage({
|
|
372
|
+
input: 300_000,
|
|
373
|
+
output: 1_000,
|
|
374
|
+
cached: 100_000,
|
|
375
|
+
cacheWrite: 100_000
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
expect(ModelMix.calculateCostBreakdown('gpt-5.6-luna', tokens)).to.deep.equal({
|
|
379
|
+
uncachedInput: 0.04,
|
|
380
|
+
cachedInput: 0.004,
|
|
381
|
+
cacheWrite: 0.05,
|
|
382
|
+
cacheWrite5m: 0,
|
|
383
|
+
cacheWrite1h: 0,
|
|
384
|
+
output: 0.0018,
|
|
385
|
+
total: 0.0958
|
|
386
|
+
});
|
|
387
|
+
expect(ModelMix.calculateCacheMetrics('gpt-5.6-luna', tokens)).to.deep.equal({
|
|
388
|
+
cacheSavings: 0.036,
|
|
389
|
+
cacheWritePremium: 0.01,
|
|
390
|
+
breakEvenHits: 0.2778
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should not apply GPT-5.6 long-context multipliers at exactly 272K input tokens', function () {
|
|
395
|
+
const tokens = ModelMix.normalizeTokenUsage({
|
|
396
|
+
input: 272_000,
|
|
397
|
+
output: 1_000,
|
|
398
|
+
cached: 100_000,
|
|
399
|
+
cacheWrite: 100_000
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
expect(ModelMix.calculateCostBreakdown('gpt-5.6-luna', tokens)).to.deep.equal({
|
|
403
|
+
uncachedInput: 0.0144,
|
|
404
|
+
cachedInput: 0.002,
|
|
405
|
+
cacheWrite: 0.025,
|
|
406
|
+
cacheWrite5m: 0,
|
|
407
|
+
cacheWrite1h: 0,
|
|
408
|
+
output: 0.0012,
|
|
409
|
+
total: 0.0426
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should price Anthropic 5-minute and 1-hour cache writes separately', function () {
|
|
414
|
+
const tokens = MixAnthropic.extractTokens({
|
|
415
|
+
usage: {
|
|
416
|
+
input_tokens: 10,
|
|
417
|
+
output_tokens: 5,
|
|
418
|
+
cache_read_input_tokens: 400,
|
|
419
|
+
cache_creation_input_tokens: 300,
|
|
420
|
+
cache_creation: {
|
|
421
|
+
ephemeral_5m_input_tokens: 100,
|
|
422
|
+
ephemeral_1h_input_tokens: 200
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(tokens).to.include({
|
|
428
|
+
input: 710,
|
|
429
|
+
cached: 400,
|
|
430
|
+
cacheWrite: 300,
|
|
431
|
+
cacheWrite5m: 100,
|
|
432
|
+
cacheWrite1h: 200,
|
|
433
|
+
uncachedInput: 10
|
|
434
|
+
});
|
|
435
|
+
expect(ModelMix.calculateCostBreakdown('claude-haiku-4-5-20251001', tokens)).to.deep.equal({
|
|
436
|
+
uncachedInput: 0.00001,
|
|
437
|
+
cachedInput: 0.00004,
|
|
438
|
+
cacheWrite: 0.000525,
|
|
439
|
+
cacheWrite5m: 0.000125,
|
|
440
|
+
cacheWrite1h: 0.0004,
|
|
441
|
+
output: 0.000025,
|
|
442
|
+
total: 0.0006
|
|
443
|
+
});
|
|
444
|
+
expect(ModelMix.calculateCacheMetrics('claude-haiku-4-5-20251001', tokens)).to.deep.equal({
|
|
445
|
+
cacheSavings: 0.00036,
|
|
446
|
+
cacheWritePremium: 0.000225,
|
|
447
|
+
breakEvenHits: 0.8333
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should expose normalized cache costs through raw() and lastRaw', async function () {
|
|
452
|
+
const provider = new MixCustom();
|
|
453
|
+
provider.create = async () => ({
|
|
454
|
+
message: 'ok',
|
|
455
|
+
think: null,
|
|
456
|
+
toolCalls: [],
|
|
457
|
+
tokens: MixOpenAIResponses.extractResponsesTokens({
|
|
458
|
+
usage: {
|
|
459
|
+
input_tokens: 1200,
|
|
460
|
+
output_tokens: 50,
|
|
461
|
+
total_tokens: 1250,
|
|
462
|
+
input_tokens_details: {
|
|
463
|
+
cached_tokens: 1024,
|
|
464
|
+
cache_write_tokens: 0
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}),
|
|
468
|
+
response: {}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const model = ModelMix.new()
|
|
472
|
+
.attach('gpt-5.6-luna', provider)
|
|
473
|
+
.addText('test');
|
|
474
|
+
const result = await model.raw();
|
|
475
|
+
|
|
476
|
+
expect(result.tokens).to.include({
|
|
477
|
+
input: 1200,
|
|
478
|
+
output: 50,
|
|
479
|
+
total: 1250,
|
|
480
|
+
cached: 1024,
|
|
481
|
+
cacheWrite: 0,
|
|
482
|
+
uncachedInput: 176,
|
|
483
|
+
cacheHitRate: 0.8533,
|
|
484
|
+
cacheSavings: 0.00018432,
|
|
485
|
+
cacheWritePremium: 0,
|
|
486
|
+
breakEvenHits: 0,
|
|
487
|
+
cost: 0.00011568
|
|
488
|
+
});
|
|
489
|
+
expect(result.tokens.costBreakdown).to.deep.equal({
|
|
490
|
+
uncachedInput: 0.0000352,
|
|
491
|
+
cachedInput: 0.00002048,
|
|
492
|
+
cacheWrite: 0,
|
|
493
|
+
cacheWrite5m: 0,
|
|
494
|
+
cacheWrite1h: 0,
|
|
495
|
+
output: 0.00006,
|
|
496
|
+
total: 0.00011568
|
|
497
|
+
});
|
|
498
|
+
expect(model.lastRaw.tokens).to.deep.equal(result.tokens);
|
|
106
499
|
});
|
|
107
500
|
|
|
108
501
|
it('should register Gemini Flash shortcuts with Google provider', function () {
|
|
109
502
|
const model = ModelMix.new()
|
|
110
503
|
.gemini36flash()
|
|
111
|
-
.gemini35flash()
|
|
504
|
+
.gemini35flash()
|
|
505
|
+
.gemini35flashLite();
|
|
112
506
|
|
|
113
507
|
expect(model.models.map(({ key }) => key)).to.deep.equal([
|
|
114
508
|
'gemini-3.6-flash',
|
|
115
|
-
'gemini-3.5-flash'
|
|
509
|
+
'gemini-3.5-flash',
|
|
510
|
+
'gemini-3.5-flash-lite'
|
|
116
511
|
]);
|
|
117
512
|
expect(model.models.every(({ provider }) => provider instanceof MixGoogle)).to.equal(true);
|
|
118
513
|
expect(ModelMix.calculateCost('gemini-3.6-flash', { input: 1_000_000, output: 1_000_000 })).to.equal(9);
|
|
119
514
|
expect(ModelMix.calculateCost('gemini-3.5-flash', { input: 1_000_000, output: 1_000_000 })).to.equal(5.25);
|
|
515
|
+
expect(ModelMix.calculateCost('gemini-3.5-flash-lite', { input: 1_000_000, output: 1_000_000 })).to.equal(2.8);
|
|
120
516
|
});
|
|
121
517
|
|
|
122
518
|
it('should register MiMo shortcuts with native and OpenRouter providers', function () {
|
|
@@ -179,7 +575,7 @@ describe('Token Usage Tracking', () => {
|
|
|
179
575
|
this.timeout(30000);
|
|
180
576
|
|
|
181
577
|
const model = ModelMix.new()
|
|
182
|
-
.
|
|
578
|
+
.gpt56luna()
|
|
183
579
|
.addText('Say hi');
|
|
184
580
|
|
|
185
581
|
const result = await model.raw();
|
|
@@ -244,7 +640,7 @@ describe('Token Usage Tracking', () => {
|
|
|
244
640
|
this.timeout(60000);
|
|
245
641
|
|
|
246
642
|
const conversation = ModelMix.new({ config: { max_history: 10 } })
|
|
247
|
-
.
|
|
643
|
+
.gpt56luna();
|
|
248
644
|
|
|
249
645
|
// First turn
|
|
250
646
|
conversation.addText('My name is Alice');
|
|
@@ -269,7 +665,7 @@ describe('Token Usage Tracking', () => {
|
|
|
269
665
|
this.timeout(30000);
|
|
270
666
|
|
|
271
667
|
const model = ModelMix.new()
|
|
272
|
-
.
|
|
668
|
+
.gpt56luna()
|
|
273
669
|
.addText('Return a simple greeting');
|
|
274
670
|
|
|
275
671
|
// Using raw() to get token info
|
|
@@ -285,7 +681,7 @@ describe('Token Usage Tracking', () => {
|
|
|
285
681
|
this.timeout(90000);
|
|
286
682
|
|
|
287
683
|
const providers = [
|
|
288
|
-
{ name: 'OpenAI', create: (m) => m.
|
|
684
|
+
{ name: 'OpenAI', create: (m) => m.gpt56luna() },
|
|
289
685
|
{ name: 'Anthropic', create: (m) => m.haiku45() },
|
|
290
686
|
{ name: 'Google', create: (m) => m.gemini3flash() }
|
|
291
687
|
];
|