@@ -10,6 +10,7 @@ import {
1010 chatModelParams ,
1111 encodingNames ,
1212} from './mapping.js'
13+ import { models } from './models.js'
1314import { resolveEncoding } from './resolveEncoding.js'
1415import { EndOfText } from './specialTokens.js'
1516
@@ -287,6 +288,84 @@ describe.each(chatModelNames)('%s', (modelName) => {
287288 } )
288289} )
289290
291+ describe ( 'estimateCost functionality' , ( ) => {
292+ const gpt4oEncoding = GptEncoding . getEncodingApiForModel (
293+ 'gpt-4o' ,
294+ resolveEncoding ,
295+ )
296+ const gpt35Encoding = GptEncoding . getEncodingApiForModel (
297+ 'gpt-3.5-turbo' ,
298+ resolveEncoding ,
299+ )
300+
301+ test ( 'estimates cost correctly for gpt-4o model' , ( ) => {
302+ const tokenCount = 1_000
303+ const cost = gpt4oEncoding . estimateCost ( tokenCount )
304+
305+ // gpt-4o has $2.5 per million tokens for input and $10 per million tokens for output
306+ expect ( cost . input ) . toBeCloseTo ( 0.002_5 , 6 ) // 1000/1M * $2.5
307+ expect ( cost . output ) . toBeCloseTo ( 0.01 , 6 ) // 1000/1M * $10
308+ expect ( cost . batchInput ) . toBeCloseTo ( 0.001_25 , 6 ) // 1000/1M * $1.25
309+ expect ( cost . batchOutput ) . toBeCloseTo ( 0.005 , 6 ) // 1000/1M * $5
310+ } )
311+
312+ test ( 'estimates cost correctly for gpt-3.5-turbo model' , ( ) => {
313+ const tokenCount = 1_000
314+ const cost = gpt35Encoding . estimateCost ( tokenCount )
315+
316+ // gpt-3.5-turbo has $0.5 per million tokens for input and $1.5 per million tokens for output
317+ expect ( cost . input ) . toBeCloseTo ( 0.000_5 , 6 ) // 1000/1M * $0.5
318+ expect ( cost . output ) . toBeCloseTo ( 0.001_5 , 6 ) // 1000/1M * $1.5
319+ expect ( cost . batchInput ) . toBeCloseTo ( 0.000_25 , 6 ) // 1000/1M * $0.25
320+ expect ( cost . batchOutput ) . toBeCloseTo ( 0.000_75 , 6 ) // 1000/1M * $0.75
321+ } )
322+
323+ test ( 'allows overriding model name' , ( ) => {
324+ const tokenCount = 1_000
325+ // Use gpt-4o encoding but override with gpt-3.5-turbo model name
326+ const cost = gpt4oEncoding . estimateCost ( tokenCount , 'gpt-3.5-turbo' )
327+
328+ expect ( cost . input ) . toBeCloseTo ( 0.000_5 , 6 ) // 1000/1M * $0.5
329+ expect ( cost . output ) . toBeCloseTo ( 0.001_5 , 6 ) // 1000/1M * $1.5
330+ } )
331+
332+ test ( 'throws error when model name is not provided' , ( ) => {
333+ const encoding = GptEncoding . getEncodingApi ( 'cl100k_base' , resolveEncoding )
334+ const tokenCount = 1_000
335+
336+ // No model name was provided during initialization or function call
337+ expect ( ( ) => encoding . estimateCost ( tokenCount ) ) . toThrow (
338+ 'Model name must be provided either during initialization or passed in to the method.' ,
339+ )
340+ } )
341+
342+ test ( 'throws error for unknown model' , ( ) => {
343+ const tokenCount = 1_000
344+ expect ( ( ) =>
345+ gpt4oEncoding . estimateCost ( tokenCount , 'non-existent-model' as any ) ,
346+ ) . toThrow ( 'Unknown model: non-existent-model' )
347+ } )
348+
349+ test ( 'only includes properties that exist for the model' , ( ) => {
350+ // Find a model that only has input cost but no output cost
351+ const modelWithInputOnly = Object . entries ( models ) . find (
352+ ( [ _ , model ] ) =>
353+ model . cost ?. input !== undefined && model . cost ?. output === undefined ,
354+ )
355+
356+ if ( modelWithInputOnly ) {
357+ const [ modelName ] = modelWithInputOnly
358+ const cost = gpt4oEncoding . estimateCost ( 1_000 , modelName as any )
359+
360+ expect ( cost . input ) . toBeDefined ( )
361+ expect ( cost . output ) . toBeUndefined ( )
362+ } else {
363+ // Skip test if we can't find an appropriate model
364+ console . log ( 'Skipping test: no model with input-only cost found' )
365+ }
366+ } )
367+ } )
368+
290369function loadTestPlans ( ) {
291370 const testPlanPath = path . join ( __dirname , '../data/TestPlans.txt' )
292371 const testPlanData = fs . readFileSync ( testPlanPath , 'utf8' )
0 commit comments