modelmix 4.7.4 → 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 -89
- package/demo/demo.js +4 -4
- package/demo/prompt.md +2 -2
- package/http-client.js +15 -2
- package/index.d.ts +100 -11
- package/index.js +896 -273
- package/package.json +4 -1
- package/skills/modelmix/SKILL.md +75 -18
- 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 +409 -16
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,9 +342,160 @@ 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 () {
|
|
@@ -182,7 +575,7 @@ describe('Token Usage Tracking', () => {
|
|
|
182
575
|
this.timeout(30000);
|
|
183
576
|
|
|
184
577
|
const model = ModelMix.new()
|
|
185
|
-
.
|
|
578
|
+
.gpt56luna()
|
|
186
579
|
.addText('Say hi');
|
|
187
580
|
|
|
188
581
|
const result = await model.raw();
|
|
@@ -247,7 +640,7 @@ describe('Token Usage Tracking', () => {
|
|
|
247
640
|
this.timeout(60000);
|
|
248
641
|
|
|
249
642
|
const conversation = ModelMix.new({ config: { max_history: 10 } })
|
|
250
|
-
.
|
|
643
|
+
.gpt56luna();
|
|
251
644
|
|
|
252
645
|
// First turn
|
|
253
646
|
conversation.addText('My name is Alice');
|
|
@@ -272,7 +665,7 @@ describe('Token Usage Tracking', () => {
|
|
|
272
665
|
this.timeout(30000);
|
|
273
666
|
|
|
274
667
|
const model = ModelMix.new()
|
|
275
|
-
.
|
|
668
|
+
.gpt56luna()
|
|
276
669
|
.addText('Return a simple greeting');
|
|
277
670
|
|
|
278
671
|
// Using raw() to get token info
|
|
@@ -288,7 +681,7 @@ describe('Token Usage Tracking', () => {
|
|
|
288
681
|
this.timeout(90000);
|
|
289
682
|
|
|
290
683
|
const providers = [
|
|
291
|
-
{ name: 'OpenAI', create: (m) => m.
|
|
684
|
+
{ name: 'OpenAI', create: (m) => m.gpt56luna() },
|
|
292
685
|
{ name: 'Anthropic', create: (m) => m.haiku45() },
|
|
293
686
|
{ name: 'Google', create: (m) => m.gemini3flash() }
|
|
294
687
|
];
|