feat: Added full cost tracking to Coyote and added it to the default right prompt

This commit is contained in:
Dark-Alex-17 committed 2026-09-14 15:24:48 -06:00
1 parent bd5c0a7cdd
commit 58b0dbd812
7 files changed
+237 -25

No files matched your search

+43
View File
@@ -24,6 +24,10 @@ pub const INTERRUPTED_RESPONSE_TEXT: &str = "[Response interrupted due to error]
/// instead of thrashing every turn.
const COMPRESSION_MIN_RECLAIM_DIVISOR: usize = 5;
fn cost_is_zero(v: &f64) -> bool {
*v == 0.0
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Session {
#[serde(rename(serialize = "model", deserialize = "model"))]
@@ -101,6 +105,8 @@ pub struct Session {
auto_continue_paused: Option<String>,
#[serde(default, skip_serializing_if = "TokenUsage::is_empty")]
token_usage: TokenUsage,
#[serde(default, skip_serializing_if = "cost_is_zero")]
cost: f64,
#[serde(skip)]
model: Model,
@@ -276,6 +282,15 @@ impl Session {
self.dirty = true;
}
pub fn cost(&self) -> f64 {
self.cost
}
pub fn accumulate_cost(&mut self, cost: f64) {
self.cost += cost;
self.dirty = true;
}
pub fn has_user_messages(&self) -> bool {
self.messages.iter().any(|v| v.role.is_user())
}
@@ -357,6 +372,9 @@ impl Session {
if let Some(value) = self.token_usage.cache_creation_input_tokens {
data["cache_creation_tokens"] = value.into();
}
if self.cost != 0.0 {
data["total_cost"] = ((self.cost * 1e6).round() / 1e6).into();
}
data["messages"] = json!(self.messages);
let output = serde_yaml::to_string(&data)
@@ -467,6 +485,9 @@ impl Session {
if let Some(value) = self.token_usage.cache_creation_input_tokens {
items.push(("cache_creation_tokens", value.to_string()));
}
if self.cost != 0.0 {
items.push(("total_cost", format!("${:.4}", self.cost)));
}
let mut lines: Vec<String> = items
.iter()
@@ -1212,6 +1233,28 @@ mod tests {
assert!(!yaml.contains("token_usage"));
}
#[test]
fn session_cost_accumulates_and_survives_yaml_round_trip() {
let mut session = Session::default();
assert_eq!(session.cost(), 0.0);
session.accumulate_cost(0.0025);
session.accumulate_cost(0.001);
assert!((session.cost() - 0.0035).abs() < 1e-12);
assert!(session.dirty());
let yaml = serde_yaml::to_string(&session).unwrap();
let reloaded: Session = serde_yaml::from_str(&yaml).unwrap();
assert!((reloaded.cost() - 0.0035).abs() < 1e-12);
}
#[test]
fn session_zero_cost_is_not_serialized() {
let session = Session::default();
let yaml = serde_yaml::to_string(&session).unwrap();
assert!(!yaml.contains("cost"));
}
#[test]
fn session_export_includes_real_usage_only_when_reported() {
let empty = Session::default().export().unwrap();