diff --git a/relay-event-normalization/src/event.rs b/relay-event-normalization/src/event.rs index 2ad1c0de89..bed3fb002d 100644 --- a/relay-event-normalization/src/event.rs +++ b/relay-event-normalization/src/event.rs @@ -2293,7 +2293,6 @@ mod tests { &NormalizationConfig { ai_model_costs: Some(&ModelCosts { version: 2, - costs: vec![], models: HashMap::from([ ( "claude-2.1".to_owned(), @@ -2399,7 +2398,6 @@ mod tests { &NormalizationConfig { ai_model_costs: Some(&ModelCosts { version: 2, - costs: vec![], models: HashMap::from([ ( "claude-2.1".to_owned(), @@ -2496,7 +2494,6 @@ mod tests { &NormalizationConfig { ai_model_costs: Some(&ModelCosts { version: 2, - costs: vec![], models: HashMap::from([( "claude-2.1".to_owned(), ModelCostV2 { @@ -2580,7 +2577,6 @@ mod tests { &NormalizationConfig { ai_model_costs: Some(&ModelCosts { version: 2, - costs: vec![], models: HashMap::from([ ( "claude-2.1".to_owned(), @@ -2660,7 +2656,6 @@ mod tests { &NormalizationConfig { ai_model_costs: Some(&ModelCosts { version: 2, - costs: vec![], models: HashMap::new(), }), ..NormalizationConfig::default() @@ -2704,7 +2699,6 @@ mod tests { &NormalizationConfig { ai_model_costs: Some(&ModelCosts { version: 2, - costs: vec![], models: HashMap::new(), }), ..NormalizationConfig::default() diff --git a/relay-event-normalization/src/normalize/mod.rs b/relay-event-normalization/src/normalize/mod.rs index be264eec6b..587e6fdc17 100644 --- a/relay-event-normalization/src/normalize/mod.rs +++ b/relay-event-normalization/src/normalize/mod.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::hash::Hash; use relay_base_schema::metrics::MetricUnit; -use relay_common::glob2::LazyGlob; use relay_event_schema::protocol::{Event, VALID_PLATFORMS}; use relay_protocol::{FiniteF64, RuleCondition}; use serde::{Deserialize, Serialize}; @@ -219,25 +218,10 @@ pub struct PerformanceScoreConfig { /// A mapping of AI model types (like GPT-4) to their respective costs. /// -/// This struct supports multiple versions with different cost structures: -/// - Version 1: Array-based costs with glob pattern matching for model IDs (uses `costs` field) -/// - Version 2: Dictionary-based costs with exact model ID keys and granular token pricing (uses `models` field) +/// This struct uses a dictionary-based cost structure with exact model ID keys and granular +/// token pricing. /// -/// Example V1 JSON: -/// ```json -/// { -/// "version": 1, -/// "costs": [ -/// { -/// "modelId": "gpt-4*", -/// "forCompletion": false, -/// "costPer1kTokens": 0.03 -/// } -/// ] -/// } -/// ``` -/// -/// Example V2 JSON: +/// Example JSON: /// ```json /// { /// "version": 2, @@ -257,72 +241,35 @@ pub struct ModelCosts { /// The version of the model cost struct pub version: u16, - /// The mappings of model ID => cost (used in version 1) - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub costs: Vec, - - /// The mappings of model ID => cost as a dictionary (version 2) + /// The mappings of model ID => cost as a dictionary #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub models: HashMap, } impl ModelCosts { - const MAX_SUPPORTED_VERSION: u16 = 2; + const SUPPORTED_VERSION: u16 = 2; /// `true` if the model costs are empty and the version is supported. pub fn is_empty(&self) -> bool { - (self.costs.is_empty() && self.models.is_empty()) || !self.is_enabled() + self.models.is_empty() || !self.is_enabled() } /// `false` if measurement and metrics extraction should be skipped. pub fn is_enabled(&self) -> bool { - self.version > 0 && self.version <= ModelCosts::MAX_SUPPORTED_VERSION + self.version == Self::SUPPORTED_VERSION } /// Gets the cost per token, if defined for the given model. pub fn cost_per_token(&self, model_id: &str) -> Option { - match self.version { - 1 => { - let input_cost = self.costs.iter().find(|cost| cost.matches(model_id, false)); - let output_cost = self.costs.iter().find(|cost| cost.matches(model_id, true)); - - // V1 costs were defined per 1k tokens, so we need to convert to per token. - if input_cost.is_some() || output_cost.is_some() { - Some(ModelCostV2 { - input_per_token: input_cost.map_or(0.0, |c| c.cost_per_1k_tokens / 1000.0), - output_per_token: output_cost - .map_or(0.0, |c| c.cost_per_1k_tokens / 1000.0), - output_reasoning_per_token: 0.0, // in v1 this info is not available - input_cached_per_token: 0.0, // in v1 this info is not available - }) - } else { - None - } - } - 2 => self.models.get(model_id).copied(), - _ => None, + if self.is_enabled() { + self.models.get(model_id).copied() + } else { + None } } } -/// A mapping of AI model types (like GPT-4) to their respective costs. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCost { - pub(crate) model_id: LazyGlob, - pub(crate) for_completion: bool, - pub(crate) cost_per_1k_tokens: f64, -} - -impl ModelCost { - /// `true` if this cost definition matches the given model. - pub fn matches(&self, model_id: &str, for_completion: bool) -> bool { - self.for_completion == for_completion && self.model_id.compiled().is_match(model_id) - } -} - /// Version 2 of a mapping of AI model types (like GPT-4) to their respective costs. -/// Version 1 had some limitations, so we're moving to a more flexible format. #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ModelCostV2 { @@ -366,26 +313,6 @@ mod tests { /// Test that integer versions are handled correctly in the struct format #[test] fn test_model_cost_version_sent_as_number() { - // Test integer version 1 - let original = r#"{"version":1,"costs":[{"modelId":"babbage-002.ft","forCompletion":false,"costPer1kTokens":0.0016}]}"#; - let deserialized: ModelCosts = serde_json::from_str(original).unwrap(); - assert_debug_snapshot!( - deserialized, - @r#" - ModelCosts { - version: 1, - costs: [ - ModelCost { - model_id: LazyGlob("babbage-002.ft"), - for_completion: false, - cost_per_1k_tokens: 0.0016, - }, - ], - models: {}, - } - "#, - ); - // Test integer version 2 let original_v2 = r#"{"version":2,"models":{"gpt-4":{"inputPerToken":0.03,"outputPerToken":0.06,"outputReasoningPerToken":0.12,"inputCachedPerToken":0.015}}}"#; let deserialized_v2: ModelCosts = serde_json::from_str(original_v2).unwrap(); @@ -394,7 +321,6 @@ mod tests { @r###" ModelCosts { version: 2, - costs: [], models: { "gpt-4": ModelCostV2 { input_per_token: 0.03, @@ -408,34 +334,12 @@ mod tests { ); // Test unknown integer version - let original_unknown = r#"{"version":99,"costs":[]}"#; + let original_unknown = r#"{"version":99,"models":{}}"#; let deserialized_unknown: ModelCosts = serde_json::from_str(original_unknown).unwrap(); assert_eq!(deserialized_unknown.version, 99); assert!(!deserialized_unknown.is_enabled()); } - #[test] - fn test_model_cost_config_v1() { - let original = r#"{"version":1,"costs":[{"modelId":"babbage-002.ft","forCompletion":false,"costPer1kTokens":0.0016}]}"#; - let deserialized: ModelCosts = serde_json::from_str(original).unwrap(); - assert_debug_snapshot!(deserialized, @r###" - ModelCosts { - version: 1, - costs: [ - ModelCost { - model_id: LazyGlob("babbage-002.ft"), - for_completion: false, - cost_per_1k_tokens: 0.0016, - }, - ], - models: {}, - } - "###); - - let serialized = serde_json::to_string(&deserialized).unwrap(); - assert_eq!(&serialized, original); - } - #[test] fn test_model_cost_config_v2() { let original = r#"{"version":2,"models":{"gpt-4":{"inputPerToken":0.03,"outputPerToken":0.06,"outputReasoningPerToken":0.12,"inputCachedPerToken":0.015}}}"#; @@ -443,7 +347,6 @@ mod tests { assert_debug_snapshot!(deserialized, @r###" ModelCosts { version: 2, - costs: [], models: { "gpt-4": ModelCostV2 { input_per_token: 0.03, @@ -459,48 +362,6 @@ mod tests { assert_eq!(&serialized, original); } - #[test] - fn test_model_cost_functionality_v1_only_input_tokens() { - // Test V1 functionality - let v1_config = ModelCosts { - version: 1, - costs: vec![ModelCost { - model_id: LazyGlob::new("gpt-4*"), - for_completion: false, - cost_per_1k_tokens: 0.03, - }], - models: HashMap::new(), - }; - assert!(v1_config.is_enabled()); - let costs = v1_config.cost_per_token("gpt-4-turbo").unwrap(); - assert_eq!(costs.input_per_token * 1000.0, 0.03); // multiplying by 1000 to avoid floating point errors - assert_eq!(costs.output_per_token, 0.0); // output tokens are not defined - } - - #[test] - fn test_model_cost_functionality_v1() { - let v1_config = ModelCosts { - version: 1, - costs: vec![ - ModelCost { - model_id: LazyGlob::new("gpt-4*"), - for_completion: false, - cost_per_1k_tokens: 0.03, - }, - ModelCost { - model_id: LazyGlob::new("gpt-4*"), - for_completion: true, - cost_per_1k_tokens: 0.06, - }, - ], - models: HashMap::new(), - }; - assert!(v1_config.is_enabled()); - let costs = v1_config.cost_per_token("gpt-4").unwrap(); - assert_eq!(costs.input_per_token * 1000.0, 0.03); // multiplying by 1000 to avoid floating point errors - assert_eq!(costs.output_per_token * 1000.0, 0.06); // multiplying by 1000 to avoid floating point errors - } - #[test] fn test_model_cost_functionality_v2() { // Test V2 functionality @@ -516,7 +377,6 @@ mod tests { ); let v2_config = ModelCosts { version: 2, - costs: vec![], models: models_map, }; assert!(v2_config.is_enabled());