11//! AI cost calculation.
22
3+ use crate :: statsd:: { Counters , map_origin_to_integration, platform_tag} ;
34use crate :: { ModelCostV2 , ModelCosts } ;
45use relay_event_schema:: protocol:: {
56 Event , Measurements , OperationType , Span , SpanData , TraceContext ,
@@ -84,8 +85,19 @@ impl CalculatedCost {
8485/// Calculates the total cost for a model call.
8586///
8687/// Returns `None` if no tokens were used.
87- pub fn calculate_costs ( model_cost : & ModelCostV2 , tokens : UsedTokens ) -> Option < CalculatedCost > {
88+ pub fn calculate_costs (
89+ model_cost : & ModelCostV2 ,
90+ tokens : UsedTokens ,
91+ integration : & str ,
92+ platform : & str ,
93+ ) -> Option < CalculatedCost > {
8894 if !tokens. has_usage ( ) {
95+ relay_statsd:: metric!(
96+ counter( Counters :: GenAiCostCalculationResult ) += 1 ,
97+ result = "calculation_none" ,
98+ integration = integration,
99+ platform = platform,
100+ ) ;
89101 return None ;
90102 }
91103
@@ -103,6 +115,19 @@ pub fn calculate_costs(model_cost: &ModelCostV2, tokens: UsedTokens) -> Option<C
103115 let output = ( tokens. raw_output_tokens ( ) * model_cost. output_per_token )
104116 + ( tokens. output_reasoning_tokens * reasoning_cost) ;
105117
118+ let metric_label = match ( input, output) {
119+ ( x, y) if x < 0.0 || y < 0.0 => "calculation_negative" ,
120+ ( 0.0 , 0.0 ) => "calculation_zero" ,
121+ _ => "calculation_positive" ,
122+ } ;
123+
124+ relay_statsd:: metric!(
125+ counter( Counters :: GenAiCostCalculationResult ) += 1 ,
126+ result = metric_label,
127+ integration = integration,
128+ platform = platform,
129+ ) ;
130+
106131 Some ( CalculatedCost { input, output } )
107132}
108133
@@ -158,11 +183,18 @@ pub fn infer_ai_operation_type(op_name: &str) -> Option<&'static str> {
158183
159184/// Calculates the cost of an AI model based on the model cost and the tokens used.
160185/// Calculated cost is in US dollars.
161- fn extract_ai_model_cost_data ( model_cost : Option < & ModelCostV2 > , data : & mut SpanData ) {
186+ fn extract_ai_model_cost_data (
187+ model_cost : Option < & ModelCostV2 > ,
188+ data : & mut SpanData ,
189+ origin : Option < & str > ,
190+ platform : Option < & str > ,
191+ ) {
162192 let Some ( model_cost) = model_cost else { return } ;
163193
164194 let used_tokens = UsedTokens :: from_span_data ( & * data) ;
165- let Some ( costs) = calculate_costs ( model_cost, used_tokens) else {
195+ let integration = map_origin_to_integration ( origin) ;
196+ let platform = platform_tag ( platform) ;
197+ let Some ( costs) = calculate_costs ( model_cost, used_tokens, integration, platform) else {
166198 return ;
167199 } ;
168200
@@ -220,7 +252,13 @@ fn set_total_tokens(data: &mut SpanData) {
220252}
221253
222254/// Extract the additional data into the span
223- fn extract_ai_data ( data : & mut SpanData , duration : f64 , ai_model_costs : & ModelCosts ) {
255+ fn extract_ai_data (
256+ data : & mut SpanData ,
257+ duration : f64 ,
258+ ai_model_costs : & ModelCosts ,
259+ origin : Option < & str > ,
260+ platform : Option < & str > ,
261+ ) {
224262 // Extracts the response tokens per second
225263 if data. gen_ai_response_tokens_per_second . value ( ) . is_none ( )
226264 && duration > 0.0
@@ -244,7 +282,12 @@ fn extract_ai_data(data: &mut SpanData, duration: f64, ai_model_costs: &ModelCos
244282 . and_then ( |val| val. as_str ( ) )
245283 } )
246284 {
247- extract_ai_model_cost_data ( ai_model_costs. cost_per_token ( model_id) , data)
285+ extract_ai_model_cost_data (
286+ ai_model_costs. cost_per_token ( model_id) ,
287+ data,
288+ origin,
289+ platform,
290+ )
248291 }
249292}
250293
@@ -255,6 +298,8 @@ fn enrich_ai_span_data(
255298 measurements : & Annotated < Measurements > ,
256299 duration : f64 ,
257300 model_costs : Option < & ModelCosts > ,
301+ origin : Option < & str > ,
302+ platform : Option < & str > ,
258303) {
259304 if !is_ai_span ( span_data, span_op. value ( ) ) {
260305 return ;
@@ -267,7 +312,7 @@ fn enrich_ai_span_data(
267312 set_total_tokens ( data) ;
268313
269314 if let Some ( model_costs) = model_costs {
270- extract_ai_data ( data, duration, model_costs) ;
315+ extract_ai_data ( data, duration, model_costs, origin , platform ) ;
271316 }
272317
273318 let ai_op_type = data
@@ -294,6 +339,8 @@ pub fn enrich_ai_span(span: &mut Span, model_costs: Option<&ModelCosts>) {
294339 & span. measurements ,
295340 duration,
296341 model_costs,
342+ span. origin . as_str ( ) ,
343+ span. platform . as_str ( ) ,
297344 ) ;
298345}
299346
@@ -316,6 +363,8 @@ pub fn enrich_ai_event_data(event: &mut Event, model_costs: Option<&ModelCosts>)
316363 & event. measurements ,
317364 event_duration,
318365 model_costs,
366+ trace_context. origin . as_str ( ) ,
367+ event. platform . as_str ( ) ,
319368 ) ;
320369 }
321370 let spans = event. spans . value_mut ( ) . iter_mut ( ) . flatten ( ) ;
@@ -326,13 +375,16 @@ pub fn enrich_ai_event_data(event: &mut Event, model_costs: Option<&ModelCosts>)
326375 . get_value ( "span.duration" )
327376 . and_then ( |v| v. as_f64 ( ) )
328377 . unwrap_or ( 0.0 ) ;
378+ let span_platform = span. platform . as_str ( ) . or_else ( || event. platform . as_str ( ) ) ;
329379
330380 enrich_ai_span_data (
331381 & mut span. data ,
332382 & span. op ,
333383 & span. measurements ,
334384 span_duration,
335385 model_costs,
386+ span. origin . as_str ( ) ,
387+ span_platform,
336388 ) ;
337389 }
338390}
@@ -378,6 +430,8 @@ mod tests {
378430 input_cache_write_per_token : 1.0 ,
379431 } ,
380432 UsedTokens :: from_span_data ( & SpanData :: default ( ) ) ,
433+ "test" ,
434+ "test" ,
381435 ) ;
382436 assert ! ( cost. is_none( ) ) ;
383437 }
@@ -399,6 +453,8 @@ mod tests {
399453 output_tokens : 15.0 ,
400454 output_reasoning_tokens : 9.0 ,
401455 } ,
456+ "test" ,
457+ "test" ,
402458 )
403459 . unwrap ( ) ;
404460
@@ -428,6 +484,8 @@ mod tests {
428484 output_tokens : 15.0 ,
429485 output_reasoning_tokens : 9.0 ,
430486 } ,
487+ "test" ,
488+ "test" ,
431489 )
432490 . unwrap ( ) ;
433491
@@ -459,6 +517,8 @@ mod tests {
459517 output_tokens : 1.0 ,
460518 output_reasoning_tokens : 9.0 ,
461519 } ,
520+ "test" ,
521+ "test" ,
462522 )
463523 . unwrap ( ) ;
464524
@@ -487,6 +547,8 @@ mod tests {
487547 output_tokens : 50.0 ,
488548 output_reasoning_tokens : 10.0 ,
489549 } ,
550+ "test" ,
551+ "test" ,
490552 )
491553 . unwrap ( ) ;
492554
@@ -523,6 +585,8 @@ mod tests {
523585 input_cache_write_per_token : 0.75 ,
524586 } ,
525587 tokens,
588+ "test" ,
589+ "test" ,
526590 )
527591 . unwrap ( ) ;
528592
0 commit comments