Skip to content

Commit 5e34db1

Browse files
committed
fix: correct test structure and tiered pricing logic
- Moved tiered pricing test suites out of nested it() block so they execute - Fixed getTieredPricing() to check base context window before checking tiers - All 23 tests now pass including 8 new tiered pricing tests
1 parent 921cc45 commit 5e34db1

File tree

2 files changed

+174
-164
lines changed

2 files changed

+174
-164
lines changed

src/shared/cost.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ function getTieredPricing(
2929
}
3030
}
3131

32+
// If within base context window, use base prices
33+
if (totalInputTokens <= modelInfo.contextWindow) {
34+
return {
35+
inputPrice: modelInfo.inputPrice,
36+
outputPrice: modelInfo.outputPrice,
37+
cacheWritesPrice: modelInfo.cacheWritesPrice,
38+
cacheReadsPrice: modelInfo.cacheReadsPrice,
39+
}
40+
}
41+
3242
// Find the appropriate tier based on the total input tokens
3343
// Tiers are checked in order, and we use the first tier where the token count
3444
// is less than or equal to the tier's context window

src/utils/__tests__/cost.spec.ts

Lines changed: 164 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)