@@ -220,190 +220,190 @@ describe("Cost Utility", () => {
220220 expect ( result . totalCost ) . toBe ( 0.0105 )
221221 expect ( result . totalInputTokens ) . toBe ( 6000 ) // Total already includes cache
222222 expect ( result . totalOutputTokens ) . toBe ( 500 )
223+ } )
224+
225+ describe ( "tiered pricing" , ( ) => {
226+ const modelWithTiers : ModelInfo = {
227+ contextWindow : 200_000 ,
228+ supportsImages : true ,
229+ supportsPromptCache : true ,
230+ inputPrice : 3.0 , // $3 per million tokens (<= 200K)
231+ outputPrice : 15.0 , // $15 per million tokens (<= 200K)
232+ cacheWritesPrice : 3.75 , // $3.75 per million tokens (<= 200K)
233+ cacheReadsPrice : 0.3 , // $0.30 per million tokens (<= 200K)
234+ tiers : [
235+ {
236+ contextWindow : 1_000_000 , // 1M tokens
237+ inputPrice : 6.0 , // $6 per million tokens (> 200K)
238+ outputPrice : 22.5 , // $22.50 per million tokens (> 200K)
239+ cacheWritesPrice : 7.5 , // $7.50 per million tokens (> 200K)
240+ cacheReadsPrice : 0.6 , // $0.60 per million tokens (> 200K)
241+ } ,
242+ ] ,
243+ }
244+
245+ it ( "should use base prices when total input tokens are below 200K" , ( ) => {
246+ const result = calculateApiCostAnthropic ( modelWithTiers , 50_000 , 10_000 , 50_000 , 50_000 )
247+
248+ // Total input: 50K + 50K + 50K = 150K (below 200K threshold)
249+ // Should use base prices: $3/$15
250+ // Input cost: (3.0 / 1_000_000) * 50_000 = 0.15
251+ // Output cost: (15.0 / 1_000_000) * 10_000 = 0.15
252+ // Cache writes: (3.75 / 1_000_000) * 50_000 = 0.1875
253+ // Cache reads: (0.3 / 1_000_000) * 50_000 = 0.015
254+ // Total: 0.15 + 0.15 + 0.1875 + 0.015 = 0.5025
255+ expect ( result . totalInputTokens ) . toBe ( 150_000 )
256+ expect ( result . totalOutputTokens ) . toBe ( 10_000 )
257+ expect ( result . totalCost ) . toBeCloseTo ( 0.5025 , 6 )
258+ } )
223259
224- describe ( "tiered pricing" , ( ) => {
225- const modelWithTiers : ModelInfo = {
260+ it ( "should use tier prices when total input tokens exceed 200K" , ( ) => {
261+ const result = calculateApiCostAnthropic ( modelWithTiers , 100_000 , 20_000 , 100_000 , 100_000 )
262+
263+ // Total input: 100K + 100K + 100K = 300K (above 200K, below 1M)
264+ // Should use tier prices: $6/$22.50
265+ // Input cost: (6.0 / 1_000_000) * 100_000 = 0.6
266+ // Output cost: (22.5 / 1_000_000) * 20_000 = 0.45
267+ // Cache writes: (7.5 / 1_000_000) * 100_000 = 0.75
268+ // Cache reads: (0.6 / 1_000_000) * 100_000 = 0.06
269+ // Total: 0.6 + 0.45 + 0.75 + 0.06 = 1.86
270+ expect ( result . totalInputTokens ) . toBe ( 300_000 )
271+ expect ( result . totalOutputTokens ) . toBe ( 20_000 )
272+ expect ( result . totalCost ) . toBeCloseTo ( 1.86 , 6 )
273+ } )
274+
275+ it ( "should use the highest tier prices when exceeding all tier thresholds" , ( ) => {
276+ const result = calculateApiCostAnthropic ( modelWithTiers , 500_000 , 50_000 , 300_000 , 300_000 )
277+
278+ // Total input: 500K + 300K + 300K = 1.1M (above 1M threshold)
279+ // Should use highest tier prices: $6/$22.50 (last tier)
280+ // Input cost: (6.0 / 1_000_000) * 500_000 = 3.0
281+ // Output cost: (22.5 / 1_000_000) * 50_000 = 1.125
282+ // Cache writes: (7.5 / 1_000_000) * 300_000 = 2.25
283+ // Cache reads: (0.6 / 1_000_000) * 300_000 = 0.18
284+ // Total: 3.0 + 1.125 + 2.25 + 0.18 = 6.555
285+ expect ( result . totalInputTokens ) . toBe ( 1_100_000 )
286+ expect ( result . totalOutputTokens ) . toBe ( 50_000 )
287+ expect ( result . totalCost ) . toBeCloseTo ( 6.555 , 6 )
288+ } )
289+
290+ it ( "should handle partial tier definitions" , ( ) => {
291+ // Model where tier only overrides some prices
292+ const modelPartialTiers : ModelInfo = {
226293 contextWindow : 200_000 ,
227294 supportsImages : true ,
228295 supportsPromptCache : true ,
229- inputPrice : 3.0 , // $3 per million tokens (<= 200K)
230- outputPrice : 15.0 , // $15 per million tokens (<= 200K)
231- cacheWritesPrice : 3.75 , // $3.75 per million tokens (<= 200K)
232- cacheReadsPrice : 0.3 , // $0.30 per million tokens (<= 200K)
296+ inputPrice : 3.0 ,
297+ outputPrice : 15.0 ,
298+ cacheWritesPrice : 3.75 ,
299+ cacheReadsPrice : 0.3 ,
233300 tiers : [
234301 {
235- contextWindow : 1_000_000 , // 1M tokens
236- inputPrice : 6.0 , // $6 per million tokens (> 200K)
237- outputPrice : 22.5 , // $22.50 per million tokens (> 200K)
238- cacheWritesPrice : 7.5 , // $7.50 per million tokens (> 200K)
239- cacheReadsPrice : 0.6 , // $0.60 per million tokens (> 200K)
302+ contextWindow : 1_000_000 ,
303+ inputPrice : 6.0 , // Only input price changes
304+ // output, cacheWrites, cacheReads prices should fall back to base
240305 } ,
241306 ] ,
242307 }
243308
244- it ( "should use base prices when total input tokens are below 200K" , ( ) => {
245- const result = calculateApiCostAnthropic ( modelWithTiers , 50_000 , 10_000 , 50_000 , 50_000 )
246-
247- // Total input: 50K + 50K + 50K = 150K (below 200K threshold)
248- // Should use base prices: $3/$15
249- // Input cost: (3.0 / 1_000_000) * 50_000 = 0.15
250- // Output cost: (15.0 / 1_000_000) * 10_000 = 0.15
251- // Cache writes: (3.75 / 1_000_000) * 50_000 = 0.1875
252- // Cache reads: (0.3 / 1_000_000) * 50_000 = 0.015
253- // Total: 0.15 + 0.15 + 0.1875 + 0.015 = 0.5025
254- expect ( result . totalInputTokens ) . toBe ( 150_000 )
255- expect ( result . totalOutputTokens ) . toBe ( 10_000 )
256- expect ( result . totalCost ) . toBeCloseTo ( 0.5025 , 6 )
257- } )
258-
259- it ( "should use tier prices when total input tokens exceed 200K" , ( ) => {
260- const result = calculateApiCostAnthropic ( modelWithTiers , 100_000 , 20_000 , 100_000 , 100_000 )
261-
262- // Total input: 100K + 100K + 100K = 300K (above 200K, below 1M)
263- // Should use tier prices: $6/$22.50
264- // Input cost: (6.0 / 1_000_000) * 100_000 = 0.6
265- // Output cost: (22.5 / 1_000_000) * 20_000 = 0.45
266- // Cache writes: (7.5 / 1_000_000) * 100_000 = 0.75
267- // Cache reads: (0.6 / 1_000_000) * 100_000 = 0.06
268- // Total: 0.6 + 0.45 + 0.75 + 0.06 = 1.86
269- expect ( result . totalInputTokens ) . toBe ( 300_000 )
270- expect ( result . totalOutputTokens ) . toBe ( 20_000 )
271- expect ( result . totalCost ) . toBeCloseTo ( 1.86 , 6 )
272- } )
273-
274- it ( "should use the highest tier prices when exceeding all tier thresholds" , ( ) => {
275- const result = calculateApiCostAnthropic ( modelWithTiers , 500_000 , 50_000 , 300_000 , 300_000 )
276-
277- // Total input: 500K + 300K + 300K = 1.1M (above 1M threshold)
278- // Should use highest tier prices: $6/$22.50 (last tier)
279- // Input cost: (6.0 / 1_000_000) * 500_000 = 3.0
280- // Output cost: (22.5 / 1_000_000) * 50_000 = 1.125
281- // Cache writes: (7.5 / 1_000_000) * 300_000 = 2.25
282- // Cache reads: (0.6 / 1_000_000) * 300_000 = 0.18
283- // Total: 3.0 + 1.125 + 2.25 + 0.18 = 6.555
284- expect ( result . totalInputTokens ) . toBe ( 1_100_000 )
285- expect ( result . totalOutputTokens ) . toBe ( 50_000 )
286- expect ( result . totalCost ) . toBeCloseTo ( 6.555 , 6 )
287- } )
288-
289- it ( "should handle partial tier definitions" , ( ) => {
290- // Model where tier only overrides some prices
291- const modelPartialTiers : ModelInfo = {
292- contextWindow : 200_000 ,
293- supportsImages : true ,
294- supportsPromptCache : true ,
295- inputPrice : 3.0 ,
296- outputPrice : 15.0 ,
297- cacheWritesPrice : 3.75 ,
298- cacheReadsPrice : 0.3 ,
299- tiers : [
300- {
301- contextWindow : 1_000_000 ,
302- inputPrice : 6.0 , // Only input price changes
303- // output, cacheWrites, cacheReads prices should fall back to base
304- } ,
305- ] ,
306- }
307-
308- const result = calculateApiCostAnthropic ( modelPartialTiers , 100_000 , 20_000 , 100_000 , 100_000 )
309-
310- // Total input: 300K (uses tier)
311- // Input cost: (6.0 / 1_000_000) * 100_000 = 0.6 (tier price)
312- // Output cost: (15.0 / 1_000_000) * 20_000 = 0.3 (base price)
313- // Cache writes: (3.75 / 1_000_000) * 100_000 = 0.375 (base price)
314- // Cache reads: (0.3 / 1_000_000) * 100_000 = 0.03 (base price)
315- // Total: 0.6 + 0.3 + 0.375 + 0.03 = 1.305
316- expect ( result . totalInputTokens ) . toBe ( 300_000 )
317- expect ( result . totalOutputTokens ) . toBe ( 20_000 )
318- expect ( result . totalCost ) . toBeCloseTo ( 1.305 , 6 )
319- } )
320-
321- it ( "should handle multiple tiers correctly" , ( ) => {
322- const modelMultipleTiers : ModelInfo = {
323- contextWindow : 128_000 ,
324- supportsImages : true ,
325- supportsPromptCache : true ,
326- inputPrice : 0.075 , // <= 128K
327- outputPrice : 0.3 ,
328- tiers : [
329- {
330- contextWindow : 200_000 , // First tier
331- inputPrice : 0.15 ,
332- outputPrice : 0.6 ,
333- } ,
334- {
335- contextWindow : 1_000_000 , // Second tier
336- inputPrice : 0.3 ,
337- outputPrice : 1.2 ,
338- } ,
339- ] ,
340- }
341-
342- // Test below first threshold (128K)
343- let result = calculateApiCostAnthropic ( modelMultipleTiers , 50_000 , 10_000 )
344- expect ( result . totalCost ) . toBeCloseTo ( ( 0.075 * 50 + 0.3 * 10 ) / 1000 , 6 )
345-
346- // Test between first and second threshold (150K)
347- result = calculateApiCostAnthropic ( modelMultipleTiers , 150_000 , 10_000 )
348- expect ( result . totalCost ) . toBeCloseTo ( ( 0.15 * 150 + 0.6 * 10 ) / 1000 , 6 )
349-
350- // Test above second threshold (500K)
351- result = calculateApiCostAnthropic ( modelMultipleTiers , 500_000 , 10_000 )
352- expect ( result . totalCost ) . toBeCloseTo ( ( 0.3 * 500 + 1.2 * 10 ) / 1000 , 6 )
353- } )
309+ const result = calculateApiCostAnthropic ( modelPartialTiers , 100_000 , 20_000 , 100_000 , 100_000 )
310+
311+ // Total input: 300K (uses tier)
312+ // Input cost: (6.0 / 1_000_000) * 100_000 = 0.6 (tier price)
313+ // Output cost: (15.0 / 1_000_000) * 20_000 = 0.3 (base price)
314+ // Cache writes: (3.75 / 1_000_000) * 100_000 = 0.375 (base price)
315+ // Cache reads: (0.3 / 1_000_000) * 100_000 = 0.03 (base price)
316+ // Total: 0.6 + 0.3 + 0.375 + 0.03 = 1.305
317+ expect ( result . totalInputTokens ) . toBe ( 300_000 )
318+ expect ( result . totalOutputTokens ) . toBe ( 20_000 )
319+ expect ( result . totalCost ) . toBeCloseTo ( 1.305 , 6 )
354320 } )
355321
356- describe ( "tiered pricing for OpenAI ", ( ) => {
357- const modelWithTiers : ModelInfo = {
358- contextWindow : 200_000 ,
322+ it ( "should handle multiple tiers correctly ", ( ) => {
323+ const modelMultipleTiers : ModelInfo = {
324+ contextWindow : 128_000 ,
359325 supportsImages : true ,
360326 supportsPromptCache : true ,
361- inputPrice : 3.0 , // $3 per million tokens (<= 200K)
362- outputPrice : 15.0 , // $15 per million tokens (<= 200K)
363- cacheWritesPrice : 3.75 , // $3.75 per million tokens (<= 200K)
364- cacheReadsPrice : 0.3 , // $0.30 per million tokens (<= 200K)
327+ inputPrice : 0.075 , // <= 128K
328+ outputPrice : 0.3 ,
365329 tiers : [
366330 {
367- contextWindow : 1_000_000 , // 1M tokens
368- inputPrice : 6.0 , // $6 per million tokens (> 200K)
369- outputPrice : 22.5 , // $22.50 per million tokens (> 200K)
370- cacheWritesPrice : 7.5 , // $7.50 per million tokens (> 200K)
371- cacheReadsPrice : 0.6 , // $0.60 per million tokens (> 200K)
331+ contextWindow : 200_000 , // First tier
332+ inputPrice : 0.15 ,
333+ outputPrice : 0.6 ,
334+ } ,
335+ {
336+ contextWindow : 1_000_000 , // Second tier
337+ inputPrice : 0.3 ,
338+ outputPrice : 1.2 ,
372339 } ,
373340 ] ,
374341 }
375342
376- it ( "should use tier prices for OpenAI when total input tokens exceed threshold" , ( ) => {
377- // Total input: 300K (includes all tokens)
378- const result = calculateApiCostOpenAI ( modelWithTiers , 300_000 , 20_000 , 100_000 , 100_000 )
379-
380- // Total input is 300K (above 200K, below 1M) - uses tier pricing
381- // Non-cached input: 300K - 100K - 100K = 100K
382- // Input cost: (6.0 / 1_000_000) * 100_000 = 0.6
383- // Output cost: (22.5 / 1_000_000) * 20_000 = 0.45
384- // Cache writes: (7.5 / 1_000_000) * 100_000 = 0.75
385- // Cache reads: (0.6 / 1_000_000) * 100_000 = 0.06
386- // Total: 0.6 + 0.45 + 0.75 + 0.06 = 1.86
387- expect ( result . totalInputTokens ) . toBe ( 300_000 )
388- expect ( result . totalOutputTokens ) . toBe ( 20_000 )
389- expect ( result . totalCost ) . toBeCloseTo ( 1.86 , 6 )
390- } )
391-
392- it ( "should use base prices for OpenAI when total input tokens are below threshold" , ( ) => {
393- // Total input: 150K (includes all tokens)
394- const result = calculateApiCostOpenAI ( modelWithTiers , 150_000 , 10_000 , 50_000 , 50_000 )
395-
396- // Total input is 150K (below 200K) - uses base pricing
397- // Non-cached input: 150K - 50K - 50K = 50K
398- // Input cost: (3.0 / 1_000_000) * 50_000 = 0.15
399- // Output cost: (15.0 / 1_000_000) * 10_000 = 0.15
400- // Cache writes: (3.75 / 1_000_000) * 50_000 = 0.1875
401- // Cache reads: (0.3 / 1_000_000) * 50_000 = 0.015
402- // Total: 0.15 + 0.15 + 0.1875 + 0.015 = 0.5025
403- expect ( result . totalInputTokens ) . toBe ( 150_000 )
404- expect ( result . totalOutputTokens ) . toBe ( 10_000 )
405- expect ( result . totalCost ) . toBeCloseTo ( 0.5025 , 6 )
406- } )
343+ // Test below first threshold (128K)
344+ let result = calculateApiCostAnthropic ( modelMultipleTiers , 50_000 , 10_000 )
345+ expect ( result . totalCost ) . toBeCloseTo ( ( 0.075 * 50 + 0.3 * 10 ) / 1000 , 6 )
346+
347+ // Test between first and second threshold (150K)
348+ result = calculateApiCostAnthropic ( modelMultipleTiers , 150_000 , 10_000 )
349+ expect ( result . totalCost ) . toBeCloseTo ( ( 0.15 * 150 + 0.6 * 10 ) / 1000 , 6 )
350+
351+ // Test above second threshold (500K)
352+ result = calculateApiCostAnthropic ( modelMultipleTiers , 500_000 , 10_000 )
353+ expect ( result . totalCost ) . toBeCloseTo ( ( 0.3 * 500 + 1.2 * 10 ) / 1000 , 6 )
354+ } )
355+ } )
356+
357+ describe ( "tiered pricing for OpenAI" , ( ) => {
358+ const modelWithTiers : ModelInfo = {
359+ contextWindow : 200_000 ,
360+ supportsImages : true ,
361+ supportsPromptCache : true ,
362+ inputPrice : 3.0 , // $3 per million tokens (<= 200K)
363+ outputPrice : 15.0 , // $15 per million tokens (<= 200K)
364+ cacheWritesPrice : 3.75 , // $3.75 per million tokens (<= 200K)
365+ cacheReadsPrice : 0.3 , // $0.30 per million tokens (<= 200K)
366+ tiers : [
367+ {
368+ contextWindow : 1_000_000 , // 1M tokens
369+ inputPrice : 6.0 , // $6 per million tokens (> 200K)
370+ outputPrice : 22.5 , // $22.50 per million tokens (> 200K)
371+ cacheWritesPrice : 7.5 , // $7.50 per million tokens (> 200K)
372+ cacheReadsPrice : 0.6 , // $0.60 per million tokens (> 200K)
373+ } ,
374+ ] ,
375+ }
376+
377+ it ( "should use tier prices for OpenAI when total input tokens exceed threshold" , ( ) => {
378+ // Total input: 300K (includes all tokens)
379+ const result = calculateApiCostOpenAI ( modelWithTiers , 300_000 , 20_000 , 100_000 , 100_000 )
380+
381+ // Total input is 300K (above 200K, below 1M) - uses tier pricing
382+ // Non-cached input: 300K - 100K - 100K = 100K
383+ // Input cost: (6.0 / 1_000_000) * 100_000 = 0.6
384+ // Output cost: (22.5 / 1_000_000) * 20_000 = 0.45
385+ // Cache writes: (7.5 / 1_000_000) * 100_000 = 0.75
386+ // Cache reads: (0.6 / 1_000_000) * 100_000 = 0.06
387+ // Total: 0.6 + 0.45 + 0.75 + 0.06 = 1.86
388+ expect ( result . totalInputTokens ) . toBe ( 300_000 )
389+ expect ( result . totalOutputTokens ) . toBe ( 20_000 )
390+ expect ( result . totalCost ) . toBeCloseTo ( 1.86 , 6 )
391+ } )
392+
393+ it ( "should use base prices for OpenAI when total input tokens are below threshold" , ( ) => {
394+ // Total input: 150K (includes all tokens)
395+ const result = calculateApiCostOpenAI ( modelWithTiers , 150_000 , 10_000 , 50_000 , 50_000 )
396+
397+ // Total input is 150K (below 200K) - uses base pricing
398+ // Non-cached input: 150K - 50K - 50K = 50K
399+ // Input cost: (3.0 / 1_000_000) * 50_000 = 0.15
400+ // Output cost: (15.0 / 1_000_000) * 10_000 = 0.15
401+ // Cache writes: (3.75 / 1_000_000) * 50_000 = 0.1875
402+ // Cache reads: (0.3 / 1_000_000) * 50_000 = 0.015
403+ // Total: 0.15 + 0.15 + 0.1875 + 0.015 = 0.5025
404+ expect ( result . totalInputTokens ) . toBe ( 150_000 )
405+ expect ( result . totalOutputTokens ) . toBe ( 10_000 )
406+ expect ( result . totalCost ) . toBeCloseTo ( 0.5025 , 6 )
407407 } )
408408 } )
409409 } )
0 commit comments