@@ -12,19 +12,21 @@ function calculateAnthropicCost(
1212 // Normalize model name to handle various formats
1313 const normalizedModel = model . toLowerCase ( ) ;
1414
15- // Define pricing per million tokens (MTok)
15+ // Define pricing per million tokens (MTok) by model family prefix
1616 const pricing : Record <
1717 string ,
1818 { input : number ; output : number ; cacheWrite : number ; cacheRead : number }
1919 > = {
20- // Claude Opus 4 (most intelligent model)
21- "claude-3- opus" : {
22- input : 15 ,
23- output : 75 ,
24- cacheWrite : 18.75 ,
25- cacheRead : 1 .5,
20+ // Claude Opus 4.5 (most intelligent model)
21+ "claude-opus-4-5 " : {
22+ input : 5 ,
23+ output : 25 ,
24+ cacheWrite : 6.25 ,
25+ cacheRead : 0 .5,
2626 } ,
27- "claude-3-opus-20240229" : {
27+
28+ // Claude Opus 4 (legacy)
29+ "claude-3-opus" : {
2830 input : 15 ,
2931 output : 75 ,
3032 cacheWrite : 18.75 ,
@@ -38,18 +40,6 @@ function calculateAnthropicCost(
3840 cacheWrite : 3.75 ,
3941 cacheRead : 0.3 ,
4042 } ,
41- "claude-3-5-sonnet-20241022" : {
42- input : 3 ,
43- output : 15 ,
44- cacheWrite : 3.75 ,
45- cacheRead : 0.3 ,
46- } ,
47- "claude-3-5-sonnet-20240620" : {
48- input : 3 ,
49- output : 15 ,
50- cacheWrite : 3.75 ,
51- cacheRead : 0.3 ,
52- } ,
5343
5444 // Claude Haiku 3.5 (fastest, most cost-effective)
5545 "claude-3-5-haiku" : {
@@ -58,12 +48,6 @@ function calculateAnthropicCost(
5848 cacheWrite : 1 ,
5949 cacheRead : 0.08 ,
6050 } ,
61- "claude-3-5-haiku-20241022" : {
62- input : 0.8 ,
63- output : 4 ,
64- cacheWrite : 1 ,
65- cacheRead : 0.08 ,
66- } ,
6751
6852 // Legacy Claude 3 Haiku
6953 "claude-3-haiku" : {
@@ -72,35 +56,16 @@ function calculateAnthropicCost(
7256 cacheWrite : 0.3 ,
7357 cacheRead : 0.03 ,
7458 } ,
75- "claude-3-haiku-20240307" : {
76- input : 0.25 ,
77- output : 1.25 ,
78- cacheWrite : 0.3 ,
79- cacheRead : 0.03 ,
80- } ,
8159 } ;
8260
83- // Find matching pricing model
84- let modelPricing = pricing [ normalizedModel ] ;
61+ // Sort keys by length (longest first) to match most specific patterns first
62+ const sortedKeys = Object . keys ( pricing ) . sort ( ( a , b ) => b . length - a . length ) ;
8563
86- // If exact match not found, try to find by partial match
87- if ( ! modelPricing ) {
88- for ( const [ key , value ] of Object . entries ( pricing ) ) {
89- if ( normalizedModel . includes ( key ) || key . includes ( normalizedModel ) ) {
90- modelPricing = value ;
91- break ;
92- }
93- }
94- }
95-
96- // If still no match, try common patterns
97- if ( ! modelPricing ) {
98- if ( normalizedModel . includes ( "opus" ) ) {
99- modelPricing = pricing [ "claude-3-opus" ] ;
100- } else if ( normalizedModel . includes ( "sonnet" ) ) {
101- modelPricing = pricing [ "claude-3-5-sonnet" ] ;
102- } else if ( normalizedModel . includes ( "haiku" ) ) {
103- modelPricing = pricing [ "claude-3-5-haiku" ] ;
64+ let modelPricing = null ;
65+ for ( const prefix of sortedKeys ) {
66+ if ( normalizedModel . startsWith ( prefix ) ) {
67+ modelPricing = pricing [ prefix ] ;
68+ break ;
10469 }
10570 }
10671
@@ -167,14 +132,90 @@ function calculateAnthropicCost(
167132 } ;
168133}
169134
135+ function calculateOpenAICost (
136+ model : string ,
137+ usage : Usage ,
138+ ) : CostBreakdown | null {
139+ // Normalize model name
140+ const normalizedModel = model . toLowerCase ( ) ;
141+
142+ // Define pricing per million tokens (MTok) by model family prefix
143+ const pricing : Record < string , { input : number ; output : number } > = {
144+ // GPT-4o models (most specific first)
145+ "gpt-4o-mini" : { input : 0.15 , output : 0.6 } ,
146+ "gpt-4o" : { input : 2.5 , output : 10 } ,
147+
148+ // GPT-4 Turbo models
149+ "gpt-4-turbo" : { input : 10 , output : 30 } ,
150+
151+ // GPT-3.5 Turbo models (most specific first)
152+ "gpt-3.5-turbo-0125" : { input : 0.5 , output : 1.5 } ,
153+ "gpt-3.5-turbo-1106" : { input : 1 , output : 2 } ,
154+ "gpt-3.5-turbo" : { input : 1.5 , output : 2 } ,
155+
156+ // Base GPT-4 (fallback for other gpt-4 variants)
157+ "gpt-4" : { input : 30 , output : 60 } ,
158+ } ;
159+
160+ // Sort keys by length (longest first) to match most specific patterns first
161+ const sortedKeys = Object . keys ( pricing ) . sort ( ( a , b ) => b . length - a . length ) ;
162+
163+ let modelPricing = null ;
164+ for ( const prefix of sortedKeys ) {
165+ if ( normalizedModel . startsWith ( prefix ) ) {
166+ modelPricing = pricing [ prefix ] ;
167+ break ;
168+ }
169+ }
170+
171+ if ( ! modelPricing ) {
172+ return null ; // Unknown model
173+ }
174+
175+ // Calculate costs
176+ const inputCost = ( usage . promptTokens / 1_000_000 ) * modelPricing . input ;
177+ const outputCost = ( usage . completionTokens / 1_000_000 ) * modelPricing . output ;
178+
179+ // Build breakdown components
180+ const breakdownParts : string [ ] = [ ] ;
181+
182+ if ( usage . promptTokens > 0 ) {
183+ breakdownParts . push (
184+ `Input: ${ usage . promptTokens . toLocaleString ( ) } tokens × $${ modelPricing . input } /MTok = $${ inputCost . toFixed ( 6 ) } ` ,
185+ ) ;
186+ }
187+
188+ if ( usage . completionTokens > 0 ) {
189+ breakdownParts . push (
190+ `Output: ${ usage . completionTokens . toLocaleString ( ) } tokens × $${ modelPricing . output } /MTok = $${ outputCost . toFixed ( 6 ) } ` ,
191+ ) ;
192+ }
193+
194+ const totalCost = inputCost + outputCost ;
195+
196+ // Build final breakdown string
197+ let breakdown = `Model: ${ model } \n` ;
198+ breakdown += breakdownParts . join ( "\n" ) ;
199+ if ( breakdownParts . length > 1 ) {
200+ breakdown += `\nTotal: $${ totalCost . toFixed ( 6 ) } ` ;
201+ }
202+
203+ return {
204+ cost : totalCost ,
205+ breakdown,
206+ } ;
207+ }
208+
170209export function calculateRequestCost (
171210 provider : string ,
172211 model : string ,
173212 usage : Usage ,
174213) : CostBreakdown | null {
175- switch ( provider ) {
214+ switch ( provider . toLowerCase ( ) ) {
176215 case "anthropic" :
177216 return calculateAnthropicCost ( model , usage ) ;
217+ case "openai" :
218+ return calculateOpenAICost ( model , usage ) ;
178219 default :
179220 return null ;
180221 }
0 commit comments