cost-calculation-breakdown.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { describe, expect, test } from "vitest";
  2. import { calculateRequestCostBreakdown, type CostBreakdown } from "@/lib/utils/cost-calculation";
  3. import type { ModelPriceData } from "@/types/model-price";
  4. function makePriceData(overrides: Partial<ModelPriceData> = {}): ModelPriceData {
  5. return {
  6. input_cost_per_token: 0.000003, // $3/MTok
  7. output_cost_per_token: 0.000015, // $15/MTok
  8. cache_creation_input_token_cost: 0.00000375, // 1.25x input
  9. cache_read_input_token_cost: 0.0000003, // 0.1x input
  10. ...overrides,
  11. };
  12. }
  13. describe("calculateRequestCostBreakdown", () => {
  14. test("basic input + output tokens", () => {
  15. const result = calculateRequestCostBreakdown(
  16. { input_tokens: 1000, output_tokens: 500 },
  17. makePriceData()
  18. );
  19. expect(result.input).toBeCloseTo(0.003, 6); // 1000 * 0.000003
  20. expect(result.output).toBeCloseTo(0.0075, 6); // 500 * 0.000015
  21. expect(result.cache_creation).toBe(0);
  22. expect(result.cache_read).toBe(0);
  23. expect(result.total).toBeCloseTo(0.0105, 6);
  24. });
  25. test("cache creation (5m + 1h) + cache read", () => {
  26. const result = calculateRequestCostBreakdown(
  27. {
  28. input_tokens: 100,
  29. output_tokens: 50,
  30. cache_creation_5m_input_tokens: 200,
  31. cache_creation_1h_input_tokens: 300,
  32. cache_read_input_tokens: 1000,
  33. },
  34. makePriceData({
  35. cache_creation_input_token_cost_above_1hr: 0.000006, // 2x input
  36. })
  37. );
  38. // cache_creation = 200 * 0.00000375 + 300 * 0.000006
  39. expect(result.cache_creation).toBeCloseTo(0.00255, 6);
  40. // cache_read = 1000 * 0.0000003
  41. expect(result.cache_read).toBeCloseTo(0.0003, 6);
  42. expect(result.total).toBeCloseTo(
  43. result.input + result.output + result.cache_creation + result.cache_read,
  44. 10
  45. );
  46. });
  47. test("image tokens go to input/output buckets", () => {
  48. const result = calculateRequestCostBreakdown(
  49. {
  50. input_tokens: 100,
  51. output_tokens: 50,
  52. input_image_tokens: 500,
  53. output_image_tokens: 200,
  54. },
  55. makePriceData({
  56. input_cost_per_image_token: 0.00001,
  57. output_cost_per_image_token: 0.00005,
  58. })
  59. );
  60. // input = 100 * 0.000003 + 500 * 0.00001
  61. expect(result.input).toBeCloseTo(0.0053, 6);
  62. // output = 50 * 0.000015 + 200 * 0.00005
  63. expect(result.output).toBeCloseTo(0.01075, 6);
  64. });
  65. test("tiered pricing with context1mApplied", () => {
  66. const result = calculateRequestCostBreakdown(
  67. {
  68. input_tokens: 300000, // crosses 200k threshold
  69. output_tokens: 100,
  70. },
  71. makePriceData(),
  72. true // context1mApplied
  73. );
  74. // input: 200000 * 0.000003 + 100000 * 0.000003 * 2.0 = 0.6 + 0.6 = 1.2
  75. expect(result.input).toBeCloseTo(1.2, 4);
  76. // output: 100 tokens, below 200k threshold
  77. expect(result.output).toBeCloseTo(0.0015, 6);
  78. });
  79. test("200k tier pricing (Gemini style)", () => {
  80. const result = calculateRequestCostBreakdown(
  81. {
  82. input_tokens: 300000, // crosses 200k threshold
  83. output_tokens: 100,
  84. },
  85. makePriceData({
  86. input_cost_per_token_above_200k_tokens: 0.000006, // 2x base for >200k
  87. })
  88. );
  89. // input: 200000 * 0.000003 + 100000 * 0.000006 = 0.6 + 0.6 = 1.2
  90. expect(result.input).toBeCloseTo(1.2, 4);
  91. });
  92. test("categories sum to total", () => {
  93. const result = calculateRequestCostBreakdown(
  94. {
  95. input_tokens: 5000,
  96. output_tokens: 2000,
  97. cache_creation_input_tokens: 1000,
  98. cache_read_input_tokens: 3000,
  99. },
  100. makePriceData()
  101. );
  102. const sum = result.input + result.output + result.cache_creation + result.cache_read;
  103. expect(result.total).toBeCloseTo(sum, 10);
  104. });
  105. test("zero usage returns all zeros", () => {
  106. const result = calculateRequestCostBreakdown({}, makePriceData());
  107. expect(result).toEqual({
  108. input: 0,
  109. output: 0,
  110. cache_creation: 0,
  111. cache_read: 0,
  112. total: 0,
  113. });
  114. });
  115. test("per-request cost goes to input bucket", () => {
  116. const result = calculateRequestCostBreakdown(
  117. { input_tokens: 0 },
  118. makePriceData({ input_cost_per_request: 0.01 })
  119. );
  120. expect(result.input).toBeCloseTo(0.01, 6);
  121. expect(result.total).toBeCloseTo(0.01, 6);
  122. });
  123. test("cache_creation_input_tokens distributed by cache_ttl", () => {
  124. // When only cache_creation_input_tokens is set (no 5m/1h split),
  125. // it should be assigned based on cache_ttl
  126. const result = calculateRequestCostBreakdown(
  127. {
  128. input_tokens: 0,
  129. output_tokens: 0,
  130. cache_creation_input_tokens: 1000,
  131. cache_ttl: "1h",
  132. },
  133. makePriceData({
  134. cache_creation_input_token_cost_above_1hr: 0.000006,
  135. })
  136. );
  137. // 1000 tokens should go to 1h tier at 0.000006
  138. expect(result.cache_creation).toBeCloseTo(0.006, 6);
  139. });
  140. });