forked from dwgx/WindsurfAPI
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcache-control.test.js
More file actions
546 lines (518 loc) · 23 KB
/
Copy pathcache-control.test.js
File metadata and controls
546 lines (518 loc) · 23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { extractCachePolicy } from '../src/handlers/messages.js';
import { handleMessages } from '../src/handlers/messages.js';
import { checkin as poolCheckin, checkout as poolCheckout, poolClear } from '../src/conversation-pool.js';
// Anthropic prompt-caching markers (cache_control: { type: 'ephemeral',
// ttl?: '5m' | '1h' }) appear on tools[], system[] blocks, and
// messages[].content[] blocks. Cascade upstream doesn't speak this
// dialect — the proxy parses, summarises, and strips them so they
// don't leak into Cascade requests, then attributes the resulting
// cache_creation tokens to ephemeral_5m or ephemeral_1h based on the
// presence of any 1h marker.
describe('extractCachePolicy — strip + summarise cache_control markers', () => {
it('counts 5m markers across tools, system, messages and strips them', () => {
const body = {
tools: [
{ name: 't1', cache_control: { type: 'ephemeral' } },
{ name: 't2' },
],
system: [
{ type: 'text', text: 'sys1' },
{ type: 'text', text: 'sys2', cache_control: { type: 'ephemeral', ttl: '5m' } },
],
messages: [
{ role: 'user', content: [
{ type: 'text', text: 'hello' },
{ type: 'text', text: 'tagged', cache_control: { type: 'ephemeral' } },
] },
],
};
const policy = extractCachePolicy(body);
assert.equal(policy.breakpointCount, 3);
assert.equal(policy.has1h, false);
// markers stripped in place
assert.equal(body.tools[0].cache_control, undefined);
assert.equal(body.system[1].cache_control, undefined);
assert.equal(body.messages[0].content[1].cache_control, undefined);
});
it('flags has1h when any marker requests 1h ttl', () => {
const body = {
system: [
{ type: 'text', text: 'a', cache_control: { type: 'ephemeral', ttl: '5m' } },
{ type: 'text', text: 'b', cache_control: { type: 'ephemeral', ttl: '1h' } },
],
};
const p = extractCachePolicy(body);
assert.equal(p.breakpointCount, 2);
assert.equal(p.has1h, true);
});
it('returns zero policy and no mutation when no markers present', () => {
const body = {
tools: [{ name: 't' }],
system: [{ type: 'text', text: 'x' }],
messages: [{ role: 'user', content: 'hi' }],
};
const p = extractCachePolicy(body);
assert.equal(p.breakpointCount, 0);
assert.equal(p.has1h, false);
});
// C5: a top-level cache_control is NOT part of the official Anthropic schema
// (breakpoints live on tools/system/content blocks). It is now ignored rather
// than treated as a whole-request breakpoint, so it contributes no policy and
// cannot leak downstream (anthropicToOpenAI never spreads the raw body).
it('ignores an unofficial top-level cache_control field', () => {
const body = {
cache_control: { type: 'ephemeral', ttl: '1h' },
messages: [{ role: 'user', content: 'hi' }],
};
const p = extractCachePolicy(body);
assert.equal(p.breakpointCount, 0);
assert.equal(p.has1h, false);
});
it('does not throw on malformed bodies', () => {
assert.doesNotThrow(() => extractCachePolicy({}));
assert.doesNotThrow(() => extractCachePolicy({ tools: null, system: 'x' }));
assert.doesNotThrow(() => extractCachePolicy({ messages: [{ role: 'user', content: null }] }));
});
});
describe('handleMessages — cache_control round-trip into Anthropic usage shape', () => {
function fakeChat(usagePatch) {
return {
async handleChatCompletions(body, ctx) {
// body.__cachePolicy must reach chat.js
return {
status: 200,
body: {
id: 'chat_1', object: 'chat.completion', created: 1, model: body.model,
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: {
prompt_tokens: 10, completion_tokens: 1, total_tokens: 11,
prompt_tokens_details: { cached_tokens: 0 },
cache_creation_input_tokens: 100,
cache_read_input_tokens: 0,
...usagePatch,
},
},
};
},
};
}
it('5m markers route creation tokens to ephemeral_5m_input_tokens', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
messages: [
{ role: 'user', content: [
{ type: 'text', text: 'hi', cache_control: { type: 'ephemeral' } },
] },
],
}, fakeChat({
cache_creation_input_tokens: 100,
cache_creation: { ephemeral_5m_input_tokens: 100, ephemeral_1h_input_tokens: 0 },
}));
assert.equal(result.status, 200);
assert.equal(result.body.usage.cache_creation_input_tokens, 100);
assert.deepEqual(result.body.usage.cache_creation, {
ephemeral_5m_input_tokens: 100,
ephemeral_1h_input_tokens: 0,
});
});
it('1h markers route creation tokens to ephemeral_1h_input_tokens', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
messages: [
{ role: 'user', content: [
{ type: 'text', text: 'hi', cache_control: { type: 'ephemeral', ttl: '1h' } },
] },
],
}, fakeChat({
cache_creation_input_tokens: 200,
cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 200 },
}));
assert.equal(result.status, 200);
assert.equal(result.body.usage.cache_creation_input_tokens, 200);
assert.deepEqual(result.body.usage.cache_creation, {
ephemeral_5m_input_tokens: 0,
ephemeral_1h_input_tokens: 200,
});
});
it('cascade pool entry honours ttlHintMs longer than default', async () => {
poolClear();
const baseEntry = {
cascadeId: 'c1', sessionId: 's1', lsPort: 12345, apiKey: 'k',
createdAt: Date.now(),
};
// Default-TTL entry: should expire at the 30-min default.
poolCheckin('fp_default', { ...baseEntry });
// 1h-hint entry: should outlive the default.
poolCheckin('fp_1h', { ...baseEntry }, '', 90 * 60 * 1000);
// After 35 min the default entry is gone, the 1h entry remains.
// We can't fast-forward time without mocking; instead simulate by
// mutating lastAccess on the stored entries directly via checkout +
// re-checkin with an old timestamp, but the simpler check is just
// that the entry struct keeps the hint. Verify by checkout while
// both are still fresh (< pool default), then by the surface fact
// that the 1h-hint entry still has its hint after restore.
const entry = poolCheckout('fp_1h');
assert.equal(entry?.ttlHintMs, 90 * 60 * 1000);
poolClear();
});
it('cascade pool checkin preserves ttlHintMs when restoring without an explicit hint', () => {
poolClear();
const e = { cascadeId: 'c', sessionId: 's', lsPort: 1, apiKey: 'k', ttlHintMs: 90 * 60 * 1000 };
poolCheckin('fp1', e);
const got = poolCheckout('fp1');
assert.equal(got.ttlHintMs, 90 * 60 * 1000);
poolClear();
});
it('emits both flat fields and nested split when no markers were sent', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
messages: [{ role: 'user', content: 'hi' }],
}, fakeChat({
cache_creation_input_tokens: 50,
}));
assert.equal(result.status, 200);
const u = result.body.usage;
// Both shapes coexist; the flat total equals the split sum.
assert.equal(u.cache_creation_input_tokens, 50);
assert.equal(u.cache_read_input_tokens, 0);
assert.equal(
u.cache_creation.ephemeral_5m_input_tokens + u.cache_creation.ephemeral_1h_input_tokens,
u.cache_creation_input_tokens,
);
});
// B8: upstream supplies a FLAT cache_creation number with no per-TTL split.
// When the request marked a 1h prefix, the flat number must route to the 1h
// bucket (per cachePolicy.has1h) instead of the old unconditional 5m default.
it('routes an upstream FLAT cache_creation to the 1h bucket when the request marked 1h (B8)', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: 'cached prefix', cache_control: { type: 'ephemeral', ttl: '1h' } }],
messages: [{ role: 'user', content: 'hi' }],
}, fakeChat({
// Real upstream number (wins over the estimate), FLAT with no split.
cache_creation_input_tokens: 321,
cache_creation: undefined,
}));
assert.equal(result.status, 200);
const u = result.body.usage;
assert.equal(u.cache_creation_input_tokens, 321);
assert.equal(u.cache_creation.ephemeral_1h_input_tokens, 321, 'flat number routed to 1h per has1h');
assert.equal(u.cache_creation.ephemeral_5m_input_tokens, 0);
});
it('routes an upstream FLAT cache_creation to the 5m bucket by default (B8)', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: 'cached prefix', cache_control: { type: 'ephemeral' } }],
messages: [{ role: 'user', content: 'hi' }],
}, fakeChat({
cache_creation_input_tokens: 210,
cache_creation: undefined,
}));
assert.equal(result.status, 200);
const u = result.body.usage;
assert.equal(u.cache_creation_input_tokens, 210);
assert.equal(u.cache_creation.ephemeral_5m_input_tokens, 210, 'flat number defaults to 5m when no 1h marker');
assert.equal(u.cache_creation.ephemeral_1h_input_tokens, 0);
});
});
// When the upstream reports NO cache tokens (DEVIN_CONNECT free tier never
// does), but the request marked a cacheable prefix with cache_control, the
// proxy falls back to a LOCAL, CJK-aware estimate of the prefix size so
// Claude Code sees a non-zero, deterministic cache_creation_input_tokens
// instead of a misleading 0. A real upstream number always wins over the
// estimate. (unverified: local estimate — true values need a paid account to
// calibrate; see PAID ledger task E.)
describe('extractCachePolicy — local cache-prefix token estimate', () => {
it('estimates the cumulative prefix at the deepest breakpoint', () => {
const body = {
system: [
{ type: 'text', text: 'aaaa aaaa aaaa aaaa' },
{ type: 'text', text: 'bbbb bbbb', cache_control: { type: 'ephemeral' } },
],
messages: [
{ role: 'user', content: [{ type: 'text', text: 'not cached, after the breakpoint' }] },
],
};
const p = extractCachePolicy(body);
assert.equal(p.breakpointCount, 1);
// Prefix = both system blocks (cumulative up to + including the marked one),
// NOT the trailing user message after the breakpoint.
assert.ok(p.estCacheCreationTokens > 0);
const prefixOnly =
Math.ceil('aaaa aaaa aaaa aaaa'.length / 4) + Math.ceil('bbbb bbbb'.length / 4);
assert.equal(p.estCacheCreationTokens, prefixOnly);
});
it('weights CJK content ~1 token/char in the prefix estimate', () => {
const cjk = '你好世界你好世界'; // 8 CJK chars → ~8 tokens
const ascii = 'abcd'.repeat(2); // 8 ASCII chars → ~2 tokens
const cjkBody = {
system: [{ type: 'text', text: cjk, cache_control: { type: 'ephemeral' } }],
};
const asciiBody = {
system: [{ type: 'text', text: ascii, cache_control: { type: 'ephemeral' } }],
};
const pc = extractCachePolicy(cjkBody);
const pa = extractCachePolicy(asciiBody);
assert.equal(pc.estCacheCreationTokens, 8);
assert.equal(pa.estCacheCreationTokens, 2);
// Same char count, but CJK estimated far higher (no ~4× undercount).
assert.ok(pc.estCacheCreationTokens > pa.estCacheCreationTokens);
});
it('includes tool name/description/schema tokens in the prefix', () => {
const body = {
tools: [
{
name: 'search',
description: 'find things',
input_schema: { type: 'object', properties: { q: { type: 'string' } } },
cache_control: { type: 'ephemeral' },
},
],
};
const p = extractCachePolicy(body);
assert.equal(p.breakpointCount, 1);
assert.ok(p.estCacheCreationTokens > 0);
});
it('returns zero estimate when no breakpoint is present', () => {
const p = extractCachePolicy({
system: [{ type: 'text', text: 'plenty of uncached text here' }],
messages: [{ role: 'user', content: 'hi' }],
});
assert.equal(p.breakpointCount, 0);
assert.equal(p.estCacheCreationTokens, 0);
});
});
describe('handleMessages — local cache estimate fallback when upstream reports none', () => {
// A cacheable prefix that clears the C2 minimum (Sonnet/Opus ~1024 tokens).
// ASCII estimates at ~chars/4, so ~5000 chars → ~1250 tokens > 1024.
const BIG_ASCII_PREFIX = 'cached system prompt block here. '.repeat(160); // >1024 est
// Upstream returns NO usage at all (DEVIN_CONNECT free tier shape) — no
// prompt_tokens, no cache fields — so the local estimate is the only source
// and the B7 clamp (min against promptTotal) is a no-op here.
function bareUsageChat() {
return {
async handleChatCompletions(body) {
return {
status: 200,
body: {
id: 'chat_1', object: 'chat.completion', created: 1, model: body.model,
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { completion_tokens: 1 },
},
};
},
};
}
it('passes __cachePolicy through to chat.js when a breakpoint is present', async () => {
let seenPolicy;
await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: 'cached system prompt block here', cache_control: { type: 'ephemeral' } }],
messages: [{ role: 'user', content: 'hi' }],
}, {
async handleChatCompletions(body) {
seenPolicy = body.__cachePolicy;
return {
status: 200,
body: {
id: 'c', object: 'chat.completion', created: 1, model: body.model,
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 10, completion_tokens: 1, total_tokens: 11 },
},
};
},
});
assert.ok(seenPolicy, '__cachePolicy must reach chat.js');
assert.ok(seenPolicy.estCacheCreationTokens > 0);
});
it('fills cache_creation_input_tokens from the local estimate (5m default)', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: BIG_ASCII_PREFIX, cache_control: { type: 'ephemeral' } }],
messages: [{ role: 'user', content: 'hi' }],
}, bareUsageChat());
assert.equal(result.status, 200);
const u = result.body.usage;
// Non-zero estimate instead of a misleading 0 (prefix clears the C2 floor).
assert.ok(u.cache_creation_input_tokens > 0);
assert.equal(u.cache_read_input_tokens, 0);
// Default ttl 5m bucket carries the estimate.
assert.equal(u.cache_creation.ephemeral_5m_input_tokens, u.cache_creation_input_tokens);
assert.equal(u.cache_creation.ephemeral_1h_input_tokens, 0);
// Invariant preserved.
assert.equal(
u.cache_creation.ephemeral_5m_input_tokens + u.cache_creation.ephemeral_1h_input_tokens,
u.cache_creation_input_tokens,
);
});
// C2: a small cacheable prefix (below the model minimum) writes NO cache
// entry — the emitted cache_creation must be 0, not a phantom estimate. The
// breakpoint is still counted for policy (pool TTL hint) upstream.
it('floors sub-threshold prefixes to 0 cache_creation (C2)', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: 'tiny cached prefix', cache_control: { type: 'ephemeral' } }],
messages: [{ role: 'user', content: 'hi' }],
}, bareUsageChat());
assert.equal(result.status, 200);
const u = result.body.usage;
assert.equal(u.cache_creation_input_tokens, 0, 'prefix under the ~1024 min → 0 creation');
assert.equal(u.cache_creation.ephemeral_5m_input_tokens, 0);
assert.equal(u.cache_creation.ephemeral_1h_input_tokens, 0);
});
it('routes the estimate to the 1h bucket when a 1h marker is present', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: BIG_ASCII_PREFIX, cache_control: { type: 'ephemeral', ttl: '1h' } }],
messages: [{ role: 'user', content: 'hi' }],
}, bareUsageChat());
assert.equal(result.status, 200);
const u = result.body.usage;
assert.ok(u.cache_creation_input_tokens > 0);
assert.equal(u.cache_creation.ephemeral_1h_input_tokens, u.cache_creation_input_tokens);
assert.equal(u.cache_creation.ephemeral_5m_input_tokens, 0);
assert.equal(
u.cache_creation.ephemeral_5m_input_tokens + u.cache_creation.ephemeral_1h_input_tokens,
u.cache_creation_input_tokens,
);
});
// C6: a request that mixes a 5m and a 1h breakpoint must split the estimated
// prefix per TTL (each breakpoint owns the incremental prefix since the last
// one), not collapse everything into the 1h bucket via the old has1h binary.
it('splits a mixed 5m + 1h prefix estimate per TTL bucket (C6)', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [
{ type: 'text', text: BIG_ASCII_PREFIX, cache_control: { type: 'ephemeral', ttl: '5m' } },
{ type: 'text', text: BIG_ASCII_PREFIX, cache_control: { type: 'ephemeral', ttl: '1h' } },
],
messages: [{ role: 'user', content: 'hi' }],
}, bareUsageChat());
assert.equal(result.status, 200);
const u = result.body.usage;
// Both buckets are populated (mixed TTL), not just the 1h one.
assert.ok(u.cache_creation.ephemeral_5m_input_tokens > 0, '5m segment attributed');
assert.ok(u.cache_creation.ephemeral_1h_input_tokens > 0, '1h segment attributed');
// Mutual exclusion / invariant: buckets sum to the flat total.
assert.equal(
u.cache_creation.ephemeral_5m_input_tokens + u.cache_creation.ephemeral_1h_input_tokens,
u.cache_creation_input_tokens,
);
// Two equal segments → roughly balanced split (within rounding).
assert.ok(
Math.abs(u.cache_creation.ephemeral_5m_input_tokens - u.cache_creation.ephemeral_1h_input_tokens) <= 2,
'equal-sized 5m/1h segments split roughly evenly',
);
});
// B6 + B7: when the estimate is substituted, the three buckets
// (input_tokens / cache_creation / cache_read) must be mutually exclusive —
// the prefix must not be double-counted into input_tokens — and the estimate
// must be clamped so cache_creation never exceeds the total prompt.
it('keeps input/creation/read mutually exclusive and clamps the estimate (B6/B7)', async () => {
// Upstream reports a prompt_tokens SMALLER than the raw prefix estimate to
// exercise the clamp; no cache fields so the local estimate is used.
const clampChat = {
async handleChatCompletions(body) {
return {
status: 200,
body: {
id: 'chat_1', object: 'chat.completion', created: 1, model: body.model,
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 500, completion_tokens: 1, total_tokens: 501 },
},
};
},
};
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: BIG_ASCII_PREFIX, cache_control: { type: 'ephemeral' } }],
messages: [{ role: 'user', content: 'hi' }],
}, clampChat);
assert.equal(result.status, 200);
const u = result.body.usage;
// B7: clamped to promptTotal (500), not the larger raw estimate.
assert.ok(u.cache_creation_input_tokens <= 500, 'creation clamped to promptTotal');
assert.ok(u.cache_creation_input_tokens > 0);
// B6: input + creation + read never exceeds the total prompt (no double
// count of the prefix into input_tokens).
assert.ok(
u.input_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens <= 500,
'three buckets stay within promptTotal (mutually exclusive)',
);
});
it('CJK cache_control content is estimated with CJK weighting in usage', async () => {
// CJK weights ~1 token/char, so ~1200 CJK chars clears the C2 floor;
// the same char count in ASCII (~chars/4) does NOT, so it emits 0. This
// still demonstrates the CJK weighting (CJK creation > ASCII creation).
const sameCharCount = 1200;
const asciiResult = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: 'a'.repeat(sameCharCount), cache_control: { type: 'ephemeral' } }],
messages: [{ role: 'user', content: 'hi' }],
}, bareUsageChat());
const cjkResult = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: '你'.repeat(sameCharCount), cache_control: { type: 'ephemeral' } }],
messages: [{ role: 'user', content: 'hi' }],
}, bareUsageChat());
// Same char count but CJK estimated far higher than ASCII.
assert.ok(
cjkResult.body.usage.cache_creation_input_tokens >
asciiResult.body.usage.cache_creation_input_tokens,
);
});
it('does NOT override a real upstream cache number with the estimate', async () => {
const upstreamChat = {
async handleChatCompletions(body) {
return {
status: 200,
body: {
id: 'chat_1', object: 'chat.completion', created: 1, model: body.model,
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: {
prompt_tokens: 10, completion_tokens: 1, total_tokens: 11,
cache_creation_input_tokens: 999,
cache_read_input_tokens: 0,
},
},
};
},
};
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
system: [{ type: 'text', text: 'cached system prompt block here', cache_control: { type: 'ephemeral' } }],
messages: [{ role: 'user', content: 'hi' }],
}, upstreamChat);
// Real upstream value wins; the estimate is not substituted.
assert.equal(result.body.usage.cache_creation_input_tokens, 999);
});
it('emits no cache fields beyond the existing shape when there is no cache_control (byte-identical behavior)', async () => {
const result = await handleMessages({
model: 'claude-sonnet-4.6',
max_tokens: 16,
messages: [{ role: 'user', content: 'hi' }],
}, bareUsageChat());
const u = result.body.usage;
// No breakpoint → no estimate → cache fields stay zero exactly as before.
assert.equal(u.cache_creation_input_tokens, 0);
assert.equal(u.cache_read_input_tokens, 0);
assert.equal(u.cache_creation.ephemeral_5m_input_tokens, 0);
assert.equal(u.cache_creation.ephemeral_1h_input_tokens, 0);
});
});