Skip to content

Commit 25c9e3f

Browse files
committed
Show credit usage upon user turn completion
1 parent 7ad7b30 commit 25c9e3f

File tree

6 files changed

+126
-10
lines changed

6 files changed

+126
-10
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: 8 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,8 @@ 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 { usage, unit, unit_plural, .. },
671+
) => ChatResponseStream::MeteringEvent { usage, unit, unit_plural },
670672
amzn_codewhisperer_streaming_client::types::ChatResponseStream::ToolUseEvent(
671673
amzn_codewhisperer_streaming_client::types::ToolUseEvent {
672674
tool_use_id,
@@ -733,8 +735,8 @@ impl From<amzn_qdeveloper_streaming_client::types::ChatResponseStream> for ChatR
733735
cache_write_input_tokens: token_usage.as_ref().and_then(|t| t.cache_write_input_tokens),
734736
},
735737
amzn_qdeveloper_streaming_client::types::ChatResponseStream::MeteringEvent(
736-
amzn_qdeveloper_streaming_client::types::MeteringEvent { usage, .. },
737-
) => ChatResponseStream::MeteringEvent { usage },
738+
amzn_qdeveloper_streaming_client::types::MeteringEvent { usage, unit, unit_plural, .. },
739+
) => ChatResponseStream::MeteringEvent { usage, unit, unit_plural },
738740
amzn_qdeveloper_streaming_client::types::ChatResponseStream::ToolUseEvent(
739741
amzn_qdeveloper_streaming_client::types::ToolUseEvent {
740742
tool_use_id,
@@ -940,7 +942,7 @@ impl From<UserInputMessage> for amzn_codewhisperer_streaming_client::types::User
940942
.set_user_input_message_context(value.user_input_message_context.map(Into::into))
941943
.set_user_intent(value.user_intent.map(Into::into))
942944
.set_model_id(value.model_id)
943-
.origin(amzn_codewhisperer_streaming_client::types::Origin::KiroCli)
945+
.origin(amzn_codewhisperer_streaming_client::types::Origin::AiEditor)
944946
.build()
945947
.expect("Failed to build UserInputMessage")
946948
}
@@ -954,7 +956,7 @@ impl From<UserInputMessage> for amzn_qdeveloper_streaming_client::types::UserInp
954956
.set_user_input_message_context(value.user_input_message_context.map(Into::into))
955957
.set_user_intent(value.user_intent.map(Into::into))
956958
.set_model_id(value.model_id)
957-
.origin(amzn_qdeveloper_streaming_client::types::Origin::KiroCli)
959+
.origin(amzn_qdeveloper_streaming_client::types::Origin::AiEditor)
958960
.build()
959961
.expect("Failed to build UserInputMessage")
960962
}

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

Lines changed: 35 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,31 @@ 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 { value, unit, unit_plural });
167+
}
168+
169+
pub fn total_usage(&self) -> Vec<MeteringUsageInfo> {
170+
let mut totals = std::collections::HashMap::new();
171+
for usage in &self.usage_info {
172+
let entry = totals.entry(&usage.unit_plural).or_insert(MeteringUsageInfo {
173+
value: 0.0,
174+
unit: usage.unit.clone(),
175+
unit_plural: usage.unit_plural.clone(),
176+
});
177+
entry.value += usage.value;
178+
}
179+
totals.into_values().collect()
180+
}
181+
182+
pub fn total_elapsed_time_ms(&self) -> Option<u64> {
183+
if let (Some(first), Some(last)) = (self.requests.first(), self.requests.last()) {
184+
Some(last.stream_end_timestamp_ms - first.request_start_timestamp_ms)
185+
} else {
186+
None
187+
}
188+
}
154189
}
155190

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

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ 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;
18+
mod turn_summary;
1719
mod parse;
1820
use std::path::MAIN_SEPARATOR;
1921
pub mod checkpoint;
@@ -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,9 @@ impl ChatSession {
28062813
trace!("Consumed: {:?}", msg_event);
28072814

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

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,8 +622,18 @@ impl ResponseParser {
622622
ChatResponseStream::ToolUseEvent { input, .. } => {
623623
self.received_response_size += input.as_ref().map(String::len).unwrap_or_default();
624624
},
625+
ChatResponseStream::MeteringEvent { usage, unit, unit_plural } => {
626+
info!("GenerateAssistanceResponse - MeteringEvent");
627+
if let (Some(value), Some(unit), Some(unit_plural)) = (usage, unit, unit_plural) {
628+
let _ = self.event_tx.send(Ok(ResponseEvent::MeteringUsage {
629+
value: *value,
630+
unit: unit.clone(),
631+
unit_plural: unit_plural.clone()
632+
})).await;
633+
}
634+
},
625635
_ => {
626-
warn!(?r, "received unexpected event from the response stream");
636+
warn!(?r, "received unexpected event from the response stream");
627637
},
628638
}
629639
}
@@ -691,6 +701,8 @@ pub enum ResponseEvent {
691701
/// Metadata for the request stream.
692702
request_metadata: RequestMetadata,
693703
},
704+
/// Metering usage consumed for this request
705+
MeteringUsage { value: f64, unit: String, unit_plural: String },
694706
}
695707

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

0 commit comments

Comments
 (0)