Skip to content

Commit 18b2ef4

Browse files
committed
🤖 test: add regression tests for step providerMetadata and cache creation
Cover the fix to prevent regression: - StreamingMessageAggregator: test step providerMetadata storage/retrieval/clear - displayUsage: test cacheCreationInputTokens extraction from providerMetadata _Generated with mux_
1 parent fd8a296 commit 18b2ef4

File tree

4 files changed

+112
-9
lines changed

4 files changed

+112
-9
lines changed

‎src/browser/utils/messages/StreamingMessageAggregator.test.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,40 @@ describe("StreamingMessageAggregator", () => {
517517
expect(aggregator.getActiveStreamCumulativeProviderMetadata("msg-1")).toBeUndefined();
518518
});
519519

520+
test("stores and retrieves step providerMetadata for cache creation display", () => {
521+
const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT);
522+
523+
aggregator.handleUsageDelta({
524+
type: "usage-delta",
525+
workspaceId: "ws-1",
526+
messageId: "msg-1",
527+
usage: { inputTokens: 1000, outputTokens: 50, totalTokens: 1050 },
528+
cumulativeUsage: { inputTokens: 1000, outputTokens: 50, totalTokens: 1050 },
529+
providerMetadata: {
530+
anthropic: { cacheCreationInputTokens: 800 },
531+
},
532+
});
533+
534+
expect(aggregator.getActiveStreamStepProviderMetadata("msg-1")).toEqual({
535+
anthropic: { cacheCreationInputTokens: 800 },
536+
});
537+
});
538+
539+
test("step providerMetadata is undefined when not provided", () => {
540+
const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT);
541+
542+
aggregator.handleUsageDelta({
543+
type: "usage-delta",
544+
workspaceId: "ws-1",
545+
messageId: "msg-1",
546+
usage: { inputTokens: 1000, outputTokens: 50, totalTokens: 1050 },
547+
cumulativeUsage: { inputTokens: 1000, outputTokens: 50, totalTokens: 1050 },
548+
// No providerMetadata
549+
});
550+
551+
expect(aggregator.getActiveStreamStepProviderMetadata("msg-1")).toBeUndefined();
552+
});
553+
520554
test("clearTokenState clears all usage tracking (step, cumulative, metadata)", () => {
521555
const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT);
522556

@@ -526,18 +560,21 @@ describe("StreamingMessageAggregator", () => {
526560
messageId: "msg-1",
527561
usage: { inputTokens: 1000, outputTokens: 50, totalTokens: 1050 },
528562
cumulativeUsage: { inputTokens: 1000, outputTokens: 50, totalTokens: 1050 },
563+
providerMetadata: { anthropic: { cacheCreationInputTokens: 300 } },
529564
cumulativeProviderMetadata: { anthropic: { cacheCreationInputTokens: 500 } },
530565
});
531566

532567
// All should be defined
533568
expect(aggregator.getActiveStreamUsage("msg-1")).toBeDefined();
569+
expect(aggregator.getActiveStreamStepProviderMetadata("msg-1")).toBeDefined();
534570
expect(aggregator.getActiveStreamCumulativeUsage("msg-1")).toBeDefined();
535571
expect(aggregator.getActiveStreamCumulativeProviderMetadata("msg-1")).toBeDefined();
536572

537573
aggregator.clearTokenState("msg-1");
538574

539575
// All should be cleared
540576
expect(aggregator.getActiveStreamUsage("msg-1")).toBeUndefined();
577+
expect(aggregator.getActiveStreamStepProviderMetadata("msg-1")).toBeUndefined();
541578
expect(aggregator.getActiveStreamCumulativeUsage("msg-1")).toBeUndefined();
542579
expect(aggregator.getActiveStreamCumulativeProviderMetadata("msg-1")).toBeUndefined();
543580
});

‎src/common/types/stream.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,14 @@ export interface UsageDeltaEvent {
142142
type: "usage-delta";
143143
workspaceId: string;
144144
messageId: string;
145-
// This step's usage (inputTokens = current context size, for context window display)
145+
146+
// Step-level: this step only (for context window display)
146147
usage: LanguageModelV2Usage;
147-
// Cumulative usage across all steps so far (for live cost display)
148+
providerMetadata?: Record<string, unknown>;
149+
150+
// Cumulative: sum across all steps (for live cost display)
148151
cumulativeUsage: LanguageModelV2Usage;
149-
// Cumulative provider metadata across all steps (for live cost display with cache tokens)
150152
cumulativeProviderMetadata?: Record<string, unknown>;
151-
// This step's provider metadata (for context window cache display)
152-
providerMetadata?: Record<string, unknown>;
153153
}
154154

155155
export type AIServiceEvent =

‎src/common/utils/tokens/displayUsage.test.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,4 +276,68 @@ describe("createDisplayUsage", () => {
276276
expect(result!.input.tokens).toBe(1000);
277277
expect(result!.cached.tokens).toBe(0);
278278
});
279+
280+
describe("Anthropic cache creation tokens from providerMetadata", () => {
281+
// Cache creation tokens are Anthropic-specific and only available in
282+
// providerMetadata.anthropic.cacheCreationInputTokens, not in LanguageModelV2Usage.
283+
// This is critical for liveUsage display during streaming.
284+
285+
test("extracts cacheCreationInputTokens from providerMetadata", () => {
286+
const usage: LanguageModelV2Usage = {
287+
inputTokens: 1000,
288+
outputTokens: 50,
289+
totalTokens: 1050,
290+
};
291+
292+
const result = createDisplayUsage(usage, "anthropic:claude-sonnet-4-20250514", {
293+
anthropic: { cacheCreationInputTokens: 800 },
294+
});
295+
296+
expect(result).toBeDefined();
297+
expect(result!.cacheCreate.tokens).toBe(800);
298+
});
299+
300+
test("cacheCreate is 0 when providerMetadata is undefined", () => {
301+
const usage: LanguageModelV2Usage = {
302+
inputTokens: 1000,
303+
outputTokens: 50,
304+
totalTokens: 1050,
305+
};
306+
307+
const result = createDisplayUsage(usage, "anthropic:claude-sonnet-4-20250514");
308+
309+
expect(result).toBeDefined();
310+
expect(result!.cacheCreate.tokens).toBe(0);
311+
});
312+
313+
test("cacheCreate is 0 when anthropic metadata lacks cacheCreationInputTokens", () => {
314+
const usage: LanguageModelV2Usage = {
315+
inputTokens: 1000,
316+
outputTokens: 50,
317+
totalTokens: 1050,
318+
};
319+
320+
const result = createDisplayUsage(usage, "anthropic:claude-sonnet-4-20250514", {
321+
anthropic: { someOtherField: 123 },
322+
});
323+
324+
expect(result).toBeDefined();
325+
expect(result!.cacheCreate.tokens).toBe(0);
326+
});
327+
328+
test("handles gateway Anthropic model with cache creation", () => {
329+
const usage: LanguageModelV2Usage = {
330+
inputTokens: 2000,
331+
outputTokens: 100,
332+
totalTokens: 2100,
333+
};
334+
335+
const result = createDisplayUsage(usage, "mux-gateway:anthropic/claude-sonnet-4-5", {
336+
anthropic: { cacheCreationInputTokens: 1500 },
337+
});
338+
339+
expect(result).toBeDefined();
340+
expect(result!.cacheCreate.tokens).toBe(1500);
341+
});
342+
});
279343
});

‎src/node/services/streamManager.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -969,10 +969,12 @@ export class StreamManager extends EventEmitter {
969969
type: "usage-delta",
970970
workspaceId: workspaceId as string,
971971
messageId: streamInfo.messageId,
972-
usage: finishStepPart.usage, // For context window display
973-
cumulativeUsage: streamInfo.cumulativeUsage, // For live cost display
974-
cumulativeProviderMetadata: streamInfo.cumulativeProviderMetadata, // For live cache costs
975-
providerMetadata: finishStepPart.providerMetadata, // For context window cache display
972+
// Step-level (for context window display)
973+
usage: finishStepPart.usage,
974+
providerMetadata: finishStepPart.providerMetadata,
975+
// Cumulative (for live cost display)
976+
cumulativeUsage: streamInfo.cumulativeUsage,
977+
cumulativeProviderMetadata: streamInfo.cumulativeProviderMetadata,
976978
};
977979
this.emit("usage-delta", usageEvent);
978980
break;

0 commit comments

Comments
 (0)