Skip to content

Commit 6a004da

Browse files
authored
Show credit usage upon user turn completion (#3382)
* Show credit usage upon user turn completion
1 parent 685cc9a commit 6a004da

File tree

6 files changed

+172
-9
lines changed

6 files changed

+172
-9
lines changed

crates/chat-cli/src/api_client/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ impl ApiClient {
365365
let request = self
366366
.client
367367
.list_available_models()
368-
.set_origin(Some(Origin::KiroCli))
368+
.set_origin(Some(Origin::AiEditor))
369369
.set_profile_arn(self.profile.as_ref().map(|p| p.arn.clone()));
370370
let mut paginator = request.into_paginator().send();
371371

@@ -458,7 +458,7 @@ impl ApiClient {
458458
) -> Result<amzn_codewhisperer_client::operation::get_usage_limits::GetUsageLimitsOutput, ApiClientError> {
459459
self.client
460460
.get_usage_limits()
461-
.set_origin(Some(amzn_codewhisperer_client::types::Origin::KiroCli))
461+
.set_origin(Some(amzn_codewhisperer_client::types::Origin::AiEditor))
462462
.send()
463463
.await
464464
.map_err(ApiClientError::GetUsageLimitsError)
@@ -543,7 +543,7 @@ impl ApiClient {
543543
match client
544544
.send_message()
545545
.conversation_state(conversation_state)
546-
.set_source(Some(QDeveloperOrigin::KiroCli))
546+
.set_source(Some(QDeveloperOrigin::AiEditor))
547547
.send()
548548
.await
549549
{

crates/chat-cli/src/api_client/model.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,8 @@ pub enum ChatResponseStream {
579579
},
580580
MeteringEvent {
581581
usage: Option<f64>,
582+
unit: Option<String>,
583+
unit_plural: Option<String>,
582584
},
583585
SupplementaryWebLinksEvent(()),
584586
ToolUseEvent {
@@ -665,8 +667,17 @@ impl From<amzn_codewhisperer_streaming_client::types::ChatResponseStream> for Ch
665667
cache_write_input_tokens: token_usage.as_ref().and_then(|t| t.cache_write_input_tokens),
666668
},
667669
amzn_codewhisperer_streaming_client::types::ChatResponseStream::MeteringEvent(
668-
amzn_codewhisperer_streaming_client::types::MeteringEvent { usage, .. },
669-
) => ChatResponseStream::MeteringEvent { usage },
670+
amzn_codewhisperer_streaming_client::types::MeteringEvent {
671+
usage,
672+
unit,
673+
unit_plural,
674+
..
675+
},
676+
) => ChatResponseStream::MeteringEvent {
677+
usage,
678+
unit,
679+
unit_plural,
680+
},
670681
amzn_codewhisperer_streaming_client::types::ChatResponseStream::ToolUseEvent(
671682
amzn_codewhisperer_streaming_client::types::ToolUseEvent {
672683
tool_use_id,
@@ -733,8 +744,17 @@ impl From<amzn_qdeveloper_streaming_client::types::ChatResponseStream> for ChatR
733744
cache_write_input_tokens: token_usage.as_ref().and_then(|t| t.cache_write_input_tokens),
734745
},
735746
amzn_qdeveloper_streaming_client::types::ChatResponseStream::MeteringEvent(
736-
amzn_qdeveloper_streaming_client::types::MeteringEvent { usage, .. },
737-
) => ChatResponseStream::MeteringEvent { usage },
747+
amzn_qdeveloper_streaming_client::types::MeteringEvent {
748+
usage,
749+
unit,
750+
unit_plural,
751+
..
752+
},
753+
) => ChatResponseStream::MeteringEvent {
754+
usage,
755+
unit,
756+
unit_plural,
757+
},
738758
amzn_qdeveloper_streaming_client::types::ChatResponseStream::ToolUseEvent(
739759
amzn_qdeveloper_streaming_client::types::ToolUseEvent {
740760
tool_use_id,
@@ -940,7 +960,7 @@ impl From<UserInputMessage> for amzn_codewhisperer_streaming_client::types::User
940960
.set_user_input_message_context(value.user_input_message_context.map(Into::into))
941961
.set_user_intent(value.user_intent.map(Into::into))
942962
.set_model_id(value.model_id)
943-
.origin(amzn_codewhisperer_streaming_client::types::Origin::KiroCli)
963+
.origin(amzn_codewhisperer_streaming_client::types::Origin::AiEditor)
944964
.build()
945965
.expect("Failed to build UserInputMessage")
946966
}
@@ -954,7 +974,7 @@ impl From<UserInputMessage> for amzn_qdeveloper_streaming_client::types::UserInp
954974
.set_user_input_message_context(value.user_input_message_context.map(Into::into))
955975
.set_user_intent(value.user_intent.map(Into::into))
956976
.set_model_id(value.model_id)
957-
.origin(amzn_qdeveloper_streaming_client::types::Origin::KiroCli)
977+
.origin(amzn_qdeveloper_streaming_client::types::Origin::AiEditor)
958978
.build()
959979
.expect("Failed to build UserInputMessage")
960980
}

crates/chat-cli/src/cli/chat/conversation.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,20 @@ pub struct McpServerInfo {
102102
pub config: CustomToolConfig,
103103
}
104104

105+
#[derive(Debug, Clone, Serialize, Deserialize)]
106+
pub struct MeteringUsageInfo {
107+
pub value: f64,
108+
pub unit: String,
109+
pub unit_plural: String,
110+
}
111+
105112
#[derive(Debug, Clone, Serialize, Deserialize)]
106113
pub struct UserTurnMetadata {
107114
continuation_id: String,
108115
/// [RequestMetadata] about the ongoing operation.
109116
requests: Vec<RequestMetadata>,
117+
/// Individual usage info consumed in this user turn
118+
pub usage_info: Vec<MeteringUsageInfo>,
110119
}
111120

112121
impl Default for UserTurnMetadata {
@@ -121,6 +130,7 @@ impl UserTurnMetadata {
121130
Self {
122131
continuation_id: uuid::Uuid::new_v4().to_string(),
123132
requests: vec![],
133+
usage_info: vec![],
124134
}
125135
}
126136

@@ -151,6 +161,35 @@ impl UserTurnMetadata {
151161
pub fn last(&self) -> Option<&RequestMetadata> {
152162
self.requests.last()
153163
}
164+
165+
pub fn add_usage(&mut self, value: f64, unit: String, unit_plural: String) {
166+
self.usage_info.push(MeteringUsageInfo {
167+
value,
168+
unit,
169+
unit_plural,
170+
});
171+
}
172+
173+
pub fn total_usage(&self) -> Vec<MeteringUsageInfo> {
174+
let mut totals = std::collections::HashMap::new();
175+
for usage in &self.usage_info {
176+
let entry = totals.entry(&usage.unit_plural).or_insert(MeteringUsageInfo {
177+
value: 0.0,
178+
unit: usage.unit.clone(),
179+
unit_plural: usage.unit_plural.clone(),
180+
});
181+
entry.value += usage.value;
182+
}
183+
totals.into_values().collect()
184+
}
185+
186+
pub fn total_elapsed_time_ms(&self) -> Option<u64> {
187+
if let (Some(first), Some(last)) = (self.requests.first(), self.requests.last()) {
188+
Some(last.stream_end_timestamp_ms - first.request_start_timestamp_ms)
189+
} else {
190+
None
191+
}
192+
}
154193
}
155194

156195
/// Tracks state related to an ongoing conversation.

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ use spinners::{
88
use crate::api_client::error::ConverseStreamErrorKind;
99
use crate::theme::StyledText;
1010
use crate::util::ui::should_send_structured_message;
11+
1112
pub mod cli;
1213
mod consts;
1314
pub mod context;
1415
mod conversation;
1516
mod input_source;
1617
mod message;
1718
mod parse;
19+
mod turn_summary;
1820
use std::path::MAIN_SEPARATOR;
1921
pub mod checkpoint;
2022
mod line_tracker;
@@ -823,6 +825,11 @@ impl ChatSession {
823825
let mut ctrl_c_stream = self.ctrlc_rx.resubscribe();
824826
let result = match self.inner.take().expect("state must always be Some") {
825827
ChatState::PromptUser { skip_printing_tools } => {
828+
// Show turn complete when we're back to prompting and no tools pending
829+
if self.tool_uses.is_empty() {
830+
turn_summary::display_turn_usage_summary(&mut self.stderr, &self.conversation.user_turn_metadata)?;
831+
}
832+
826833
match (self.interactive, self.tool_uses.is_empty()) {
827834
(false, true) => {
828835
self.inner = Some(ChatState::Exit);
@@ -2806,6 +2813,15 @@ impl ChatSession {
28062813
trace!("Consumed: {:?}", msg_event);
28072814

28082815
match msg_event {
2816+
parser::ResponseEvent::MeteringUsage {
2817+
value,
2818+
unit,
2819+
unit_plural,
2820+
} => {
2821+
self.conversation
2822+
.user_turn_metadata
2823+
.add_usage(value, unit.clone(), unit_plural.clone());
2824+
},
28092825
parser::ResponseEvent::ToolUseStart { name } => {
28102826
// We need to flush the buffer here, otherwise text will not be
28112827
// printed while we are receiving tool use events.

crates/chat-cli/src/cli/chat/parser.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,23 @@ impl ResponseParser {
622622
ChatResponseStream::ToolUseEvent { input, .. } => {
623623
self.received_response_size += input.as_ref().map(String::len).unwrap_or_default();
624624
},
625+
ChatResponseStream::MeteringEvent {
626+
usage,
627+
unit,
628+
unit_plural,
629+
} => {
630+
info!("GenerateAssistanceResponse - MeteringEvent");
631+
if let (Some(value), Some(unit), Some(unit_plural)) = (usage, unit, unit_plural) {
632+
let _ = self
633+
.event_tx
634+
.send(Ok(ResponseEvent::MeteringUsage {
635+
value: *value,
636+
unit: unit.clone(),
637+
unit_plural: unit_plural.clone(),
638+
}))
639+
.await;
640+
}
641+
},
625642
_ => {
626643
warn!(?r, "received unexpected event from the response stream");
627644
},
@@ -691,6 +708,12 @@ pub enum ResponseEvent {
691708
/// Metadata for the request stream.
692709
request_metadata: RequestMetadata,
693710
},
711+
/// Metering usage consumed for this request
712+
MeteringUsage {
713+
value: f64,
714+
unit: String,
715+
unit_plural: String,
716+
},
694717
}
695718

696719
/// Metadata about the sent request and associated response stream.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use std::io::Write;
2+
3+
use crossterm::{
4+
execute,
5+
style,
6+
};
7+
8+
use crate::cli::chat::conversation::UserTurnMetadata;
9+
10+
pub fn format_number(value: f64) -> String {
11+
format!("{:.2}", (value * 100.0).floor() / 100.0)
12+
}
13+
14+
pub fn capitalize_first(s: &str) -> String {
15+
let mut chars = s.chars();
16+
match chars.next() {
17+
None => String::new(),
18+
Some(first) => {
19+
let mut result = first.to_uppercase().collect::<String>();
20+
result.push_str(chars.as_str());
21+
result
22+
},
23+
}
24+
}
25+
26+
pub fn format_elapsed_time(elapsed_ms: u64) -> String {
27+
let elapsed_s = elapsed_ms / 1000;
28+
if elapsed_s < 60 {
29+
format!("{elapsed_s}s")
30+
} else {
31+
let minutes = elapsed_s / 60;
32+
let seconds = elapsed_s % 60;
33+
format!("{minutes}m {seconds}s")
34+
}
35+
}
36+
37+
pub fn display_turn_usage_summary<W: Write>(
38+
stderr: &mut W,
39+
user_turn_metadata: &UserTurnMetadata,
40+
) -> Result<(), std::io::Error> {
41+
let totals = user_turn_metadata.total_usage();
42+
let mut parts = Vec::new();
43+
44+
// Add usage info
45+
for usage in totals {
46+
let formatted = format_number(usage.value);
47+
let capitalized_unit = capitalize_first(&usage.unit_plural);
48+
parts.push(format!("{capitalized_unit} used: {formatted}"));
49+
}
50+
51+
// Add elapsed time
52+
if let Some(elapsed_ms) = user_turn_metadata.total_elapsed_time_ms() {
53+
parts.push(format!("Elapsed time: {}", format_elapsed_time(elapsed_ms)));
54+
}
55+
56+
if !parts.is_empty() {
57+
execute!(
58+
stderr,
59+
style::SetForegroundColor(style::Color::DarkGrey),
60+
style::Print(format!("{}\n", parts.join(" "))),
61+
style::ResetColor,
62+
)?;
63+
}
64+
Ok(())
65+
}

0 commit comments

Comments
 (0)