generated from nhcarrigan/template
feat: massive overhaul to manage costs (#103)
### Explanation _No response_ ### Issue Closes #102 ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: #103 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #103.
This commit is contained in:
@@ -9,12 +9,13 @@ use tempfile::NamedTempFile;
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
use crate::achievements::{get_achievement_info, AchievementUnlockedEvent};
|
||||
use crate::commands::record_cost;
|
||||
use crate::config::ClaudeStartOptions;
|
||||
use crate::stats::{StatsUpdateEvent, UsageStats};
|
||||
use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats};
|
||||
use crate::types::{
|
||||
CharacterState, ClaudeMessage, ConnectionEvent, ConnectionStatus, ContentBlock, OutputEvent,
|
||||
PermissionPromptEvent, QuestionOption, SessionEvent, StateChangeEvent, UserQuestionEvent,
|
||||
WorkingDirectoryEvent,
|
||||
CharacterState, ClaudeMessage, ConnectionEvent, ConnectionStatus, ContentBlock, MessageCost,
|
||||
OutputEvent, PermissionPromptEvent, QuestionOption, SessionEvent, StateChangeEvent,
|
||||
UserQuestionEvent, WorkingDirectoryEvent,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
@@ -534,6 +535,7 @@ fn handle_stderr(
|
||||
content: line,
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
cost: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -586,17 +588,57 @@ fn process_json_line(
|
||||
let mut state = CharacterState::Typing;
|
||||
let mut tool_name = None;
|
||||
|
||||
// Collect all tool names in this message for token attribution
|
||||
let tools_in_message: Vec<String> = message
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|block| match block {
|
||||
ContentBlock::ToolUse { name, .. } => Some(name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Track message cost for display
|
||||
let mut message_cost: Option<MessageCost> = None;
|
||||
|
||||
// Only update stats if we have usage information
|
||||
if let Some(usage) = &message.usage {
|
||||
if let Some(model) = &message.model {
|
||||
// Calculate cost for historical tracking
|
||||
let cost_usd = calculate_cost(usage.input_tokens, usage.output_tokens, model);
|
||||
|
||||
// Store cost for later use in output events
|
||||
message_cost = Some(MessageCost {
|
||||
input_tokens: usage.input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
cost_usd,
|
||||
});
|
||||
|
||||
// Batch all stats updates in a single write lock
|
||||
{
|
||||
let mut stats_guard = stats.write();
|
||||
stats_guard.increment_messages();
|
||||
stats_guard.add_usage(usage.input_tokens, usage.output_tokens, model);
|
||||
stats_guard.get_session_duration();
|
||||
|
||||
// Attribute tokens to tools if any tools were used in this message
|
||||
if !tools_in_message.is_empty() {
|
||||
let per_tool_input = usage.input_tokens / tools_in_message.len() as u64;
|
||||
let per_tool_output = usage.output_tokens / tools_in_message.len() as u64;
|
||||
for tool in &tools_in_message {
|
||||
stats_guard.add_tool_tokens(tool, per_tool_input, per_tool_output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Record to historical cost tracking
|
||||
let app_clone = app.clone();
|
||||
let input = usage.input_tokens;
|
||||
let output = usage.output_tokens;
|
||||
tauri::async_runtime::spawn(async move {
|
||||
record_cost(&app_clone, input, output, cost_usd).await;
|
||||
});
|
||||
|
||||
// Don't emit here - we'll emit on Result message instead
|
||||
// This reduces the frequency of updates
|
||||
} else {
|
||||
@@ -635,6 +677,7 @@ fn process_json_line(
|
||||
content: desc,
|
||||
tool_name: Some(name.clone()),
|
||||
conversation_id: conversation_id.clone(),
|
||||
cost: None, // Tool use doesn't have separate cost
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -652,6 +695,7 @@ fn process_json_line(
|
||||
content: text.clone(),
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
cost: message_cost.clone(), // Include cost with assistant text
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -664,6 +708,7 @@ fn process_json_line(
|
||||
content: format!("[Thinking] {}", thinking),
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
cost: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -723,9 +768,20 @@ fn process_json_line(
|
||||
stats_guard.model.clone().unwrap_or_else(|| "claude-opus-4-20250514".to_string())
|
||||
};
|
||||
|
||||
// Calculate cost for historical tracking
|
||||
let cost_usd = calculate_cost(usage_info.input_tokens, usage_info.output_tokens, &model);
|
||||
|
||||
let mut stats_guard = stats.write();
|
||||
stats_guard.add_usage(usage_info.input_tokens, usage_info.output_tokens, &model);
|
||||
println!("Result message tokens - input: {}, output: {}", usage_info.input_tokens, usage_info.output_tokens);
|
||||
|
||||
// Record to historical cost tracking
|
||||
let app_clone = app.clone();
|
||||
let input = usage_info.input_tokens;
|
||||
let output = usage_info.output_tokens;
|
||||
tauri::async_runtime::spawn(async move {
|
||||
record_cost(&app_clone, input, output, cost_usd).await;
|
||||
});
|
||||
}
|
||||
|
||||
// Always emit updated stats on result message (less frequent)
|
||||
@@ -797,6 +853,7 @@ fn process_json_line(
|
||||
content: text.clone(),
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
cost: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user