diff --git a/bindings/python/tests/test_cost_models.py b/bindings/python/tests/test_cost_models.py index facfdb0..096eead 100644 --- a/bindings/python/tests/test_cost_models.py +++ b/bindings/python/tests/test_cost_models.py @@ -5,7 +5,7 @@ - Pydantic validates Decimal type — rejects float - None means unknown (not zero) - Decimal("0") means explicitly free -- SessionStatus.estimated_cost is removed (was never populated) +- SessionStatus.estimated_cost is deprecated (was never populated) """ from decimal import Decimal diff --git a/crates/amplifier-core/src/generated/conversions.rs b/crates/amplifier-core/src/generated/conversions.rs index 39b06cf..cc95ef6 100644 --- a/crates/amplifier-core/src/generated/conversions.rs +++ b/crates/amplifier-core/src/generated/conversions.rs @@ -168,6 +168,7 @@ impl From for super::amplifier_module::Usage { i32::MAX }) }), + cost_usd: native.cost_usd.clone(), } } } @@ -181,6 +182,7 @@ impl From for crate::messages::Usage { reasoning_tokens: proto.reasoning_tokens.map(i64::from), cache_read_tokens: proto.cache_read_tokens.map(i64::from), cache_write_tokens: proto.cache_creation_tokens.map(i64::from), + cost_usd: proto.cost_usd.clone(), extensions: HashMap::new(), } } @@ -1111,6 +1113,40 @@ mod tests { ); } + /// Verify that cost_usd survives roundtrip (None and Some cases). + #[test] + fn usage_cost_usd_roundtrips_correctly() { + // Test Some case + let original_with_cost = crate::messages::Usage { + input_tokens: 100, + output_tokens: 50, + total_tokens: 150, + reasoning_tokens: None, + cache_read_tokens: None, + cache_write_tokens: None, + cost_usd: Some("0.123".to_string()), + extensions: HashMap::new(), + }; + let proto: super::super::amplifier_module::Usage = original_with_cost.clone().into(); + let restored: crate::messages::Usage = proto.into(); + assert_eq!(original_with_cost.cost_usd, restored.cost_usd); + + // Test None case + let original_without_cost = crate::messages::Usage { + input_tokens: 100, + output_tokens: 50, + total_tokens: 150, + reasoning_tokens: None, + cache_read_tokens: None, + cache_write_tokens: None, + cost_usd: None, + extensions: HashMap::new(), + }; + let proto: super::super::amplifier_module::Usage = original_without_cost.clone().into(); + let restored: crate::messages::Usage = proto.into(); + assert_eq!(original_without_cost.cost_usd, restored.cost_usd); + } + // -- E-3: ModelInfo i64→i32 overflow clamps to i32::MAX -- #[test] diff --git a/crates/amplifier-core/src/messages.rs b/crates/amplifier-core/src/messages.rs index bb0e746..f1b8b9a 100644 --- a/crates/amplifier-core/src/messages.rs +++ b/crates/amplifier-core/src/messages.rs @@ -275,6 +275,8 @@ pub struct Usage { pub cache_read_tokens: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub cache_write_tokens: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cost_usd: Option, #[serde(flatten)] pub extensions: HashMap, } diff --git a/proto/amplifier_module.proto b/proto/amplifier_module.proto index e12f988..47ff700 100644 --- a/proto/amplifier_module.proto +++ b/proto/amplifier_module.proto @@ -305,6 +305,7 @@ message Usage { optional int32 reasoning_tokens = 4; optional int32 cache_read_tokens = 5; optional int32 cache_creation_tokens = 6; + optional string cost_usd = 7; } message Degradation { diff --git a/uv.lock b/uv.lock index 652a841..c288645 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "amplifier-core" -version = "1.5.1" +version = "1.6.0" source = { editable = "." } dependencies = [ { name = "click" },