1import type { ModelTotals } from "@t3tools/shared/usageMerge";
2import { describe, expect, it } from "vite-plus/test";
3
4import {
5 cacheHitRate,
6 costPerMillionTokens,
7 modelShare,
8 sortModelsByTokens,
9} from "./usageBreakdown";
10
11const model = (
12 name: string,
13 totalTokens: number,
14 costUsd: number,
15 overrides: Partial<ModelTotals> = {},
16): ModelTotals => ({
17 model: name,
18 provider: "codex",
19 costUsd,
20 totalTokens,
21 tokens: {
22 uncachedInputTokens: totalTokens,
23 cachedInputTokens: 0,
24 cacheCreationTokens: 0,
25 outputTokens: 0,
26 reasoningTokens: 0,
27 },
28 records: 1,
29 unpricedRecords: 0,
30 unpricedTokens: 0,
31 costShare: 0,
32 tokenShare: 0,
33 ...overrides,
34});
35
36describe("sortModelsByTokens", () => {
37 it("sorts by tokens, breaks ties by cost, and leaves the input alone", () => {
38 const models = [
39 model("lower-cost", 100, 1),
40 model("more-tokens", 200, 2),
41 model("higher-cost", 100, 3),
42 ];
43
44 expect(sortModelsByTokens(models).map((item) => item.model)).toEqual([
45 "more-tokens",
46 "higher-cost",
47 "lower-cost",
48 ]);
49 expect(models.map((item) => item.model)).toEqual(["lower-cost", "more-tokens", "higher-cost"]);
50 });
51});
52
53describe("modelShare", () => {
54 it("follows the selected metric", () => {
55 const priced = model("priced", 100, 9, { costShare: 0.9, tokenShare: 0.25 });
56
57 expect(modelShare(priced, "cost")).toBe(0.9);
58 expect(modelShare(priced, "tokens")).toBe(0.25);
59 });
60
61 it("has no cost share for an unknown cost but keeps its token share", () => {
62 const unpriced = model("unpriced", 300, 0, {
63 unpricedRecords: 1,
64 unpricedTokens: 300,
65 tokenShare: 0.75,
66 });
67
68 expect(modelShare(unpriced, "cost")).toBeNull();
69 expect(modelShare(unpriced, "tokens")).toBe(0.75);
70 });
71});
72
73describe("model rates", () => {
74 it("counts cache writes as misses and leaves unpriced tokens out of $/1M", () => {
75 const mixed = model("mixed", 4_000_000, 6, {
76 tokens: {
77 uncachedInputTokens: 1_000_000,
78 cachedInputTokens: 1_000_000,
79 cacheCreationTokens: 2_000_000,
80 outputTokens: 0,
81 reasoningTokens: 0,
82 },
83 records: 4,
84 unpricedRecords: 1,
85 unpricedTokens: 1_000_000,
86 });
87
88 expect(cacheHitRate(mixed)).toBe(0.25);
89 expect(costPerMillionTokens(mixed)).toBe(2);
90 expect(costPerMillionTokens({ ...mixed, unpricedRecords: 4 })).toBeNull();
91 });
92});