cost-calculation-group-multiplier.test.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { describe, expect, test } from "vitest";
  2. import { calculateRequestCost, calculateRequestCostBreakdown } from "@/lib/utils/cost-calculation";
  3. import type { ModelPriceData } from "@/types/model-price";
  4. function makeSimplePriceData(
  5. inputCostPerToken: number,
  6. outputCostPerToken: number
  7. ): ModelPriceData {
  8. return {
  9. input_cost_per_token: inputCostPerToken,
  10. output_cost_per_token: outputCostPerToken,
  11. } as ModelPriceData;
  12. }
  13. describe("cost-calculation group multiplier", () => {
  14. const priceData = makeSimplePriceData(0.000003, 0.000015);
  15. const usage = { input_tokens: 1000, output_tokens: 500 };
  16. test("groupMultiplier=1 produces same result as without groupMultiplier", () => {
  17. const withoutGroup = calculateRequestCost(usage, priceData, {
  18. multiplier: 1.0,
  19. });
  20. const withGroup = calculateRequestCost(usage, priceData, {
  21. multiplier: 1.0,
  22. groupMultiplier: 1.0,
  23. });
  24. expect(withGroup.toNumber()).toBe(withoutGroup.toNumber());
  25. });
  26. test("compound multiplier: providerMultiplier=1.5, groupMultiplier=2.0 => baseCost * 3.0", () => {
  27. const baseCost = calculateRequestCost(usage, priceData, {
  28. multiplier: 1.0,
  29. });
  30. const compoundCost = calculateRequestCost(usage, priceData, {
  31. multiplier: 1.5,
  32. groupMultiplier: 2.0,
  33. });
  34. // 1.5 * 2.0 = 3.0
  35. expect(compoundCost.toNumber()).toBeCloseTo(baseCost.toNumber() * 3.0, 10);
  36. });
  37. test("groupMultiplier=undefined defaults to 1.0", () => {
  38. const withDefault = calculateRequestCost(usage, priceData, {
  39. multiplier: 2.0,
  40. });
  41. const withExplicit = calculateRequestCost(usage, priceData, {
  42. multiplier: 2.0,
  43. groupMultiplier: 1.0,
  44. });
  45. expect(withDefault.toNumber()).toBe(withExplicit.toNumber());
  46. });
  47. test("groupMultiplier applies independently from provider multiplier", () => {
  48. const baseCost = calculateRequestCost(usage, priceData, {
  49. multiplier: 1.0,
  50. });
  51. const groupOnly = calculateRequestCost(usage, priceData, {
  52. multiplier: 1.0,
  53. groupMultiplier: 2.0,
  54. });
  55. expect(groupOnly.toNumber()).toBeCloseTo(baseCost.toNumber() * 2.0, 10);
  56. });
  57. test("breakdown does not include multipliers (always raw)", () => {
  58. const breakdown = calculateRequestCostBreakdown(usage, priceData);
  59. // input: 1000 * 0.000003 = 0.003
  60. expect(breakdown.input).toBeCloseTo(0.003, 6);
  61. // output: 500 * 0.000015 = 0.0075
  62. expect(breakdown.output).toBeCloseTo(0.0075, 6);
  63. expect(breakdown.total).toBeCloseTo(0.0105, 6);
  64. // Verify breakdown equals the base cost (multiplier=1, no group multiplier)
  65. const baseCost = calculateRequestCost(usage, priceData, {
  66. multiplier: 1.0,
  67. });
  68. expect(breakdown.total).toBeCloseTo(baseCost.toNumber(), 10);
  69. });
  70. test("groupMultiplier=NaN falls back to 1.0", () => {
  71. const baseCost = calculateRequestCost(usage, priceData, {
  72. multiplier: 1.0,
  73. });
  74. const withNan = calculateRequestCost(usage, priceData, {
  75. multiplier: 1.0,
  76. groupMultiplier: Number.NaN,
  77. });
  78. expect(withNan.toNumber()).toBe(baseCost.toNumber());
  79. });
  80. test("groupMultiplier=Infinity falls back to 1.0", () => {
  81. const baseCost = calculateRequestCost(usage, priceData, {
  82. multiplier: 1.0,
  83. });
  84. const withInfinity = calculateRequestCost(usage, priceData, {
  85. multiplier: 1.0,
  86. groupMultiplier: Number.POSITIVE_INFINITY,
  87. });
  88. expect(withInfinity.toNumber()).toBe(baseCost.toNumber());
  89. });
  90. test("groupMultiplier=negative falls back to 1.0", () => {
  91. const baseCost = calculateRequestCost(usage, priceData, {
  92. multiplier: 1.0,
  93. });
  94. const withNegative = calculateRequestCost(usage, priceData, {
  95. multiplier: 1.0,
  96. groupMultiplier: -0.5,
  97. });
  98. expect(withNegative.toNumber()).toBe(baseCost.toNumber());
  99. });
  100. test("provider multiplier sanitizes NaN/Infinity/negative too", () => {
  101. const baseCost = calculateRequestCost(usage, priceData, {
  102. multiplier: 1.0,
  103. });
  104. const withBadProvider = calculateRequestCost(usage, priceData, {
  105. multiplier: Number.NaN,
  106. groupMultiplier: 2.0,
  107. });
  108. // NaN multiplier falls back to 1.0, so result is baseCost * 2.0
  109. expect(withBadProvider.toNumber()).toBeCloseTo(baseCost.toNumber() * 2.0, 10);
  110. });
  111. test("breakdown splits cache_creation into 5m and 1h buckets", () => {
  112. const priceDataWithCache = {
  113. input_cost_per_token: 0.000003,
  114. output_cost_per_token: 0.000015,
  115. cache_creation_input_token_cost: 0.00000375, // 1.25x input
  116. cache_creation_input_token_cost_above_1hr: 0.000006, // 2x input
  117. } as ModelPriceData;
  118. const cacheUsage = {
  119. input_tokens: 100,
  120. output_tokens: 50,
  121. cache_creation_5m_input_tokens: 1000,
  122. cache_creation_1h_input_tokens: 500,
  123. };
  124. const breakdown = calculateRequestCostBreakdown(cacheUsage, priceDataWithCache);
  125. // 5m: 1000 * 0.00000375 = 0.00375
  126. expect(breakdown.cache_creation_5m).toBeCloseTo(0.00375, 8);
  127. // 1h: 500 * 0.000006 = 0.003
  128. expect(breakdown.cache_creation_1h).toBeCloseTo(0.003, 8);
  129. // Aggregate is sum of the two
  130. expect(breakdown.cache_creation).toBeCloseTo(
  131. breakdown.cache_creation_5m + breakdown.cache_creation_1h,
  132. 10
  133. );
  134. });
  135. test("mixed TTL breakdown: 5m and 1h are distinct (no double counting)", () => {
  136. const priceDataWithCache = {
  137. input_cost_per_token: 0.000003,
  138. output_cost_per_token: 0.000015,
  139. cache_creation_input_token_cost: 0.00000375,
  140. cache_creation_input_token_cost_above_1hr: 0.000006,
  141. } as ModelPriceData;
  142. const mixedUsage = {
  143. input_tokens: 100,
  144. output_tokens: 50,
  145. cache_creation_5m_input_tokens: 2000,
  146. cache_creation_1h_input_tokens: 1000,
  147. cache_ttl: "mixed" as const,
  148. };
  149. const breakdown = calculateRequestCostBreakdown(mixedUsage, priceDataWithCache);
  150. expect(breakdown.cache_creation_5m).toBeGreaterThan(0);
  151. expect(breakdown.cache_creation_1h).toBeGreaterThan(0);
  152. expect(breakdown.cache_creation_5m).not.toBe(breakdown.cache_creation_1h);
  153. // Aggregate cache_creation equals sum of both
  154. expect(breakdown.cache_creation).toBeCloseTo(
  155. breakdown.cache_creation_5m + breakdown.cache_creation_1h,
  156. 10
  157. );
  158. });
  159. });