-
Notifications
You must be signed in to change notification settings - Fork 99
feat(ai): AI cost calculation #4840
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,43 @@ | ||
//! AI cost calculation. | ||
|
||
use crate::ModelCosts; | ||
use crate::{ModelCostV2, ModelCosts}; | ||
use relay_event_schema::protocol::{Event, Span, SpanData}; | ||
use relay_protocol::{Annotated, Value}; | ||
|
||
/// Calculates the cost of an AI model based on the model cost and the tokens used. | ||
/// Calculated cost is in US dollars. | ||
fn calculate_ai_model_cost( | ||
model_id: &str, | ||
prompt_tokens_used: Option<f64>, | ||
completion_tokens_used: Option<f64>, | ||
total_tokens_used: Option<f64>, | ||
ai_model_costs: &ModelCosts, | ||
) -> Option<f64> { | ||
if let Some(cost_per_token) = ai_model_costs.cost_per_token(model_id) { | ||
let mut result = 0.0; | ||
|
||
// If we have separate input/output token counts, use those | ||
if let Some(prompt_tokens) = prompt_tokens_used { | ||
result += cost_per_token.input_per_token * prompt_tokens; | ||
} | ||
if let Some(completion_tokens) = completion_tokens_used { | ||
result += cost_per_token.output_per_token * completion_tokens; | ||
} | ||
fn calculate_ai_model_cost(model_cost: Option<ModelCostV2>, data: &SpanData) -> Option<f64> { | ||
let cost_per_token = model_cost?; | ||
let input_tokens_used = data | ||
.gen_ai_usage_input_tokens | ||
.value() | ||
.and_then(Value::as_f64) | ||
.unwrap_or(0.0); | ||
|
||
// If we only have total tokens and no breakdown, use input cost for all tokens | ||
// (assuming it's more common to have input cost defined in V1 configs) | ||
if prompt_tokens_used.is_none() && completion_tokens_used.is_none() { | ||
if let Some(total_tokens) = total_tokens_used { | ||
if cost_per_token.input_per_token > 0.0 { | ||
result += cost_per_token.input_per_token * total_tokens; | ||
} else if cost_per_token.output_per_token > 0.0 { | ||
result += cost_per_token.output_per_token * total_tokens; | ||
} | ||
} | ||
} | ||
Comment on lines
-26
to
-36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't make sense to base our calculation on total_tokens used anymore, so this can now be removed |
||
let output_tokens_used = data | ||
.gen_ai_usage_output_tokens | ||
.value() | ||
.and_then(Value::as_f64) | ||
.unwrap_or(0.0); | ||
let output_reasoning_tokens_used = data | ||
.gen_ai_usage_output_tokens_reasoning | ||
.value() | ||
.and_then(Value::as_f64) | ||
.unwrap_or(0.0); | ||
let input_cached_tokens_used = data | ||
.gen_ai_usage_input_tokens_cached | ||
.value() | ||
.and_then(Value::as_f64) | ||
.unwrap_or(0.0); | ||
|
||
return Some(result); | ||
} | ||
None | ||
let mut result = 0.0; | ||
|
||
result += cost_per_token.input_per_token * input_tokens_used; | ||
result += cost_per_token.output_per_token * output_tokens_used; | ||
result += cost_per_token.output_reasoning_per_token * output_reasoning_tokens_used; | ||
result += cost_per_token.input_cached_per_token * input_cached_tokens_used; | ||
|
||
Some(result) | ||
} | ||
|
||
/// Maps AI-related measurements (legacy) to span data. | ||
|
@@ -94,27 +94,17 @@ pub fn extract_ai_data(span: &mut Span, ai_model_costs: &ModelCosts) { | |
return; | ||
}; | ||
|
||
let total_tokens_used = data | ||
.gen_ai_usage_total_tokens | ||
.value() | ||
.and_then(Value::as_f64); | ||
let prompt_tokens_used = data | ||
.gen_ai_usage_input_tokens | ||
.value() | ||
.and_then(Value::as_f64); | ||
let completion_tokens_used = data | ||
.gen_ai_usage_output_tokens | ||
if let Some(model_id) = data | ||
.gen_ai_request_model | ||
.value() | ||
.and_then(Value::as_f64); | ||
|
||
if let Some(model_id) = data.ai_model_id.value().and_then(|val| val.as_str()) { | ||
if let Some(total_cost) = calculate_ai_model_cost( | ||
model_id, | ||
prompt_tokens_used, | ||
completion_tokens_used, | ||
total_tokens_used, | ||
ai_model_costs, | ||
) { | ||
.and_then(|val| val.as_str()) | ||
// xxx (vgrozdanic): temporal fallback to legacy field, until we fix | ||
// sentry conventions and standardize what SDKs send | ||
.or_else(|| data.ai_model_id.value().and_then(|val| val.as_str())) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently we still want to use |
||
{ | ||
if let Some(total_cost) = | ||
calculate_ai_model_cost(ai_model_costs.cost_per_token(model_id), data) | ||
{ | ||
data.gen_ai_usage_total_cost | ||
.set_value(Value::F64(total_cost).into()); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these are legacy fields that are still being sent, until we do a cut-off and fully transition to a new agent monitoring we need to support them