|
1 | 1 | use clap::Args; |
2 | | -use crossterm::style::Attribute; |
| 2 | +use crossterm::style::{Attribute, Color}; |
3 | 3 | use crossterm::{ |
4 | 4 | execute, |
5 | 5 | queue, |
6 | 6 | style, |
7 | 7 | }; |
| 8 | +use chrono::{DateTime, Utc}; |
8 | 9 |
|
9 | 10 | use super::model::context_window_tokens; |
10 | 11 | use crate::cli::chat::token_counter::{ |
@@ -73,19 +74,189 @@ pub async fn get_total_usage_percentage(session: &mut ChatSession, os: &Os) -> R |
73 | 74 | Ok(calculate_usage_percentage(data.total_tokens, data.context_window_size)) |
74 | 75 | } |
75 | 76 |
|
| 77 | +/// Display billing and subscription information |
| 78 | +async fn display_billing_info(os: &Os, session: &mut ChatSession) -> Result<bool, ChatError> { |
| 79 | + match os.client.get_usage_limits().await { |
| 80 | + Ok(usage_limits) => { |
| 81 | + display_user_and_plan_info(&usage_limits, session).await?; |
| 82 | + display_bonus_credits(&usage_limits, session).await?; |
| 83 | + display_estimated_usage(&usage_limits, session).await?; |
| 84 | + Ok(true) |
| 85 | + }, |
| 86 | + Err(_) => { |
| 87 | + // Hide billing section when not authenticated |
| 88 | + Ok(false) |
| 89 | + } |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +async fn display_user_and_plan_info(_usage_limits: &amzn_codewhisperer_client::operation::get_usage_limits::GetUsageLimitsOutput, session: &mut ChatSession) -> Result<(), ChatError> { |
| 94 | + execute!( |
| 95 | + session.stderr, |
| 96 | + style::SetAttribute(style::Attribute::Bold), |
| 97 | + style::Print("Usage details\n"), |
| 98 | + style::SetAttribute(style::Attribute::Reset), |
| 99 | + style::Print("To manage your account, upgrade your plan or configure overages use "), |
| 100 | + style::SetForegroundColor(Color::Blue), |
| 101 | + style::Print("/usage manage"), |
| 102 | + style::SetForegroundColor(Color::Reset), |
| 103 | + style::Print(" to open admin hub\n\n"), |
| 104 | + )?; |
| 105 | + Ok(()) |
| 106 | +} |
| 107 | + |
| 108 | +async fn display_bonus_credits(usage_limits: &amzn_codewhisperer_client::operation::get_usage_limits::GetUsageLimitsOutput, session: &mut ChatSession) -> Result<(), ChatError> { |
| 109 | + let usage_breakdown = usage_limits.usage_breakdown_list(); |
| 110 | + |
| 111 | + // Find Credits resource type for bonus credits |
| 112 | + if let Some(credits) = usage_breakdown.iter().find(|item| { |
| 113 | + item.resource_type().map_or(false, |rt| rt.as_str() == "CREDIT") |
| 114 | + }) { |
| 115 | + if let Some(free_trial_info) = credits.free_trial_info() { |
| 116 | + let used = free_trial_info.current_usage().unwrap_or(0); |
| 117 | + let total = free_trial_info.usage_limit().unwrap_or(0); |
| 118 | + |
| 119 | + // Calculate days until expiry |
| 120 | + if let Some(expiry_timestamp) = free_trial_info.free_trial_expiry() { |
| 121 | + let expiry_secs = expiry_timestamp.secs(); |
| 122 | + let expiry_date = DateTime::from_timestamp(expiry_secs, 0).unwrap_or_else(|| Utc::now()); |
| 123 | + let now = Utc::now(); |
| 124 | + let days_until_expiry = (expiry_date - now).num_days().max(0); |
| 125 | + |
| 126 | + execute!( |
| 127 | + session.stderr, |
| 128 | + style::SetForegroundColor(Color::Red), |
| 129 | + style::Print("🎁 "), |
| 130 | + style::SetForegroundColor(Color::Reset), |
| 131 | + style::SetAttribute(style::Attribute::Bold), |
| 132 | + style::Print("Bonus credits: "), |
| 133 | + style::SetAttribute(style::Attribute::Reset), |
| 134 | + style::Print("You have bonus credits applied to your account, we will use these first, then your plan credits.\n"), |
| 135 | + style::Print(format!("New user credit bonus: {}/{} credits used, expires in {} days\n\n", used, total, days_until_expiry)), |
| 136 | + )?; |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + Ok(()) |
| 141 | +} |
| 142 | + |
| 143 | +async fn display_estimated_usage(usage_limits: &amzn_codewhisperer_client::operation::get_usage_limits::GetUsageLimitsOutput, session: &mut ChatSession) -> Result<(), ChatError> { |
| 144 | + let usage_breakdown = usage_limits.usage_breakdown_list(); |
| 145 | + |
| 146 | + // Get plan info |
| 147 | + let plan_name = usage_limits.subscription_info() |
| 148 | + .map(|si| si.subscription_title()) |
| 149 | + .unwrap_or("Unknown"); |
| 150 | + |
| 151 | + // Get days until reset |
| 152 | + let days_left = usage_limits.days_until_reset().unwrap_or(0); |
| 153 | + |
| 154 | + // Get credits info |
| 155 | + if let Some(credits) = usage_breakdown.iter().find(|item| { |
| 156 | + item.resource_type().map_or(false, |rt| rt.as_str() == "CREDIT") |
| 157 | + }) { |
| 158 | + let used = credits.current_usage(); |
| 159 | + let limit = credits.usage_limit(); |
| 160 | + let percentage = if limit > 0 { (used as f32 / limit as f32 * 100.0) as i32 } else { 0 }; |
| 161 | + |
| 162 | + execute!( |
| 163 | + session.stderr, |
| 164 | + style::Print(format!("Current plan: {}\n", plan_name)), |
| 165 | + style::Print("Overages: Off\n"), |
| 166 | + style::Print(format!("Days left in billing cycle: {}\n\n", days_left)), |
| 167 | + style::Print(format!("Current plan credit usage ({} of {} credits used)\n", used, limit)), |
| 168 | + )?; |
| 169 | + |
| 170 | + // Draw progress bar |
| 171 | + let bar_width = 60; |
| 172 | + let filled_width = (percentage as f32 / 100.0 * bar_width as f32) as usize; |
| 173 | + let empty_width = bar_width - filled_width; |
| 174 | + |
| 175 | + execute!( |
| 176 | + session.stderr, |
| 177 | + style::SetForegroundColor(Color::Magenta), |
| 178 | + style::Print("█".repeat(filled_width)), |
| 179 | + style::SetForegroundColor(Color::DarkGrey), |
| 180 | + style::Print("█".repeat(empty_width)), |
| 181 | + style::SetForegroundColor(Color::Reset), |
| 182 | + style::Print(format!(" {}%\n\n", percentage)), |
| 183 | + )?; |
| 184 | + } |
| 185 | + |
| 186 | + Ok(()) |
| 187 | +} |
| 188 | + |
76 | 189 | /// Arguments for the usage command that displays token usage statistics and context window |
77 | 190 | /// information. |
78 | 191 | /// |
79 | 192 | /// This command shows how many tokens are being used by different components (context files, tools, |
80 | 193 | /// assistant responses, and user prompts) within the current chat session's context window. |
81 | 194 | #[deny(missing_docs)] |
82 | 195 | #[derive(Debug, PartialEq, Args)] |
83 | | -pub struct UsageArgs; |
| 196 | +pub struct UsageArgs { |
| 197 | + /// Show only context window usage |
| 198 | + #[arg(long)] |
| 199 | + context: bool, |
| 200 | + /// Show only credits and billing information |
| 201 | + #[arg(long)] |
| 202 | + credits: bool, |
| 203 | +} |
84 | 204 |
|
85 | 205 | impl UsageArgs { |
86 | 206 | pub async fn execute(self, os: &Os, session: &mut ChatSession) -> Result<ChatState, ChatError> { |
| 207 | + match (self.context, self.credits) { |
| 208 | + (true, false) => { |
| 209 | + // Show only context window usage |
| 210 | + self.show_context_usage(os, session).await |
| 211 | + }, |
| 212 | + (false, true) => { |
| 213 | + // Show only credits/billing information |
| 214 | + self.show_credits_info(os, session).await |
| 215 | + }, |
| 216 | + (false, false) => { |
| 217 | + // Show both (default behavior) |
| 218 | + self.show_full_usage(os, session).await |
| 219 | + }, |
| 220 | + (true, true) => { |
| 221 | + // Both flags specified - show error |
| 222 | + execute!( |
| 223 | + session.stderr, |
| 224 | + style::SetForegroundColor(Color::Red), |
| 225 | + style::Print("Error: Cannot specify both --context and --credits flags\n"), |
| 226 | + style::SetForegroundColor(Color::Reset), |
| 227 | + )?; |
| 228 | + Ok(ChatState::PromptUser { skip_printing_tools: true }) |
| 229 | + } |
| 230 | + } |
| 231 | + } |
| 232 | + |
| 233 | + async fn show_context_usage(&self, os: &Os, session: &mut ChatSession) -> Result<ChatState, ChatError> { |
87 | 234 | let usage_data = get_detailed_usage_data(session, os).await?; |
| 235 | + self.display_context_window(&usage_data, session).await?; |
| 236 | + Ok(ChatState::PromptUser { skip_printing_tools: true }) |
| 237 | + } |
| 238 | + |
| 239 | + async fn show_credits_info(&self, os: &Os, session: &mut ChatSession) -> Result<ChatState, ChatError> { |
| 240 | + let billing_displayed = display_billing_info(os, session).await?; |
| 241 | + if !billing_displayed { |
| 242 | + execute!( |
| 243 | + session.stderr, |
| 244 | + style::Print("Credit based usage is not supported for your subscription\n"), |
| 245 | + )?; |
| 246 | + } |
| 247 | + Ok(ChatState::PromptUser { skip_printing_tools: true }) |
| 248 | + } |
| 249 | + |
| 250 | + async fn show_full_usage(&self, os: &Os, session: &mut ChatSession) -> Result<ChatState, ChatError> { |
| 251 | + // Try to display billing information first (silently ignore if not available) |
| 252 | + let _billing_displayed = display_billing_info(os, session).await?; |
88 | 253 |
|
| 254 | + let usage_data = get_detailed_usage_data(session, os).await?; |
| 255 | + self.display_context_window(&usage_data, session).await?; |
| 256 | + Ok(ChatState::PromptUser { skip_printing_tools: true }) |
| 257 | + } |
| 258 | + |
| 259 | + async fn display_context_window(&self, usage_data: &DetailedUsageData, session: &mut ChatSession) -> Result<(), ChatError> { |
89 | 260 | if !usage_data.dropped_context_files.is_empty() { |
90 | 261 | execute!( |
91 | 262 | session.stderr, |
@@ -252,8 +423,38 @@ impl UsageArgs { |
252 | 423 | StyledText::reset(), |
253 | 424 | )?; |
254 | 425 |
|
255 | | - Ok(ChatState::PromptUser { |
256 | | - skip_printing_tools: true, |
257 | | - }) |
| 426 | + Ok(()) |
| 427 | + } |
| 428 | +} |
| 429 | + |
| 430 | +#[cfg(test)] |
| 431 | +mod tests { |
| 432 | + use super::*; |
| 433 | + |
| 434 | + #[test] |
| 435 | + fn test_calculate_usage_percentage() { |
| 436 | + let char_count: CharCount = 4000.into(); // 4000 chars ≈ 1000 tokens |
| 437 | + let tokens: TokenCount = char_count.into(); |
| 438 | + let context_window_size = 10000; |
| 439 | + let percentage = calculate_usage_percentage(tokens, context_window_size); |
| 440 | + assert_eq!(percentage, 10.0); |
| 441 | + } |
| 442 | + |
| 443 | + #[test] |
| 444 | + fn test_calculate_usage_percentage_zero() { |
| 445 | + let char_count: CharCount = 0.into(); |
| 446 | + let tokens: TokenCount = char_count.into(); |
| 447 | + let context_window_size = 10000; |
| 448 | + let percentage = calculate_usage_percentage(tokens, context_window_size); |
| 449 | + assert_eq!(percentage, 0.0); |
| 450 | + } |
| 451 | + |
| 452 | + #[test] |
| 453 | + fn test_calculate_usage_percentage_full() { |
| 454 | + let char_count: CharCount = 40000.into(); // 40000 chars ≈ 10000 tokens |
| 455 | + let tokens: TokenCount = char_count.into(); |
| 456 | + let context_window_size = 10000; |
| 457 | + let percentage = calculate_usage_percentage(tokens, context_window_size); |
| 458 | + assert_eq!(percentage, 100.0); |
258 | 459 | } |
259 | 460 | } |
0 commit comments