@@ -8,6 +8,7 @@ use anyhow::{Context as _, Result, anyhow};
88use collections:: HashSet ;
99pub use connection:: * ;
1010pub use diff:: * ;
11+ use feature_flags:: { AcpBetaFeatureFlag , FeatureFlagAppExt as _} ;
1112use futures:: { FutureExt , channel:: oneshot, future:: BoxFuture } ;
1213use gpui:: { AppContext , AsyncApp , Context , Entity , EventEmitter , SharedString , Task , WeakEntity } ;
1314use itertools:: Itertools ;
@@ -972,7 +973,7 @@ impl PlanEntry {
972973 }
973974}
974975
975- #[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
976+ #[ derive( Debug , Clone , Default , PartialEq , Eq , Serialize , Deserialize ) ]
976977pub struct TokenUsage {
977978 pub max_tokens : u64 ,
978979 pub used_tokens : u64 ,
@@ -981,6 +982,12 @@ pub struct TokenUsage {
981982 pub max_output_tokens : Option < u64 > ,
982983}
983984
985+ #[ derive( Debug , Clone ) ]
986+ pub struct SessionCost {
987+ pub amount : f64 ,
988+ pub currency : SharedString ,
989+ }
990+
984991pub const TOKEN_USAGE_WARNING_THRESHOLD : f32 = 0.8 ;
985992
986993impl TokenUsage {
@@ -1043,6 +1050,7 @@ pub struct AcpThread {
10431050 running_turn : Option < RunningTurn > ,
10441051 connection : Rc < dyn AgentConnection > ,
10451052 token_usage : Option < TokenUsage > ,
1053+ cost : Option < SessionCost > ,
10461054 prompt_capabilities : acp:: PromptCapabilities ,
10471055 available_commands : Vec < acp:: AvailableCommand > ,
10481056 _observe_prompt_capabilities : Task < anyhow:: Result < ( ) > > ,
@@ -1232,6 +1240,7 @@ impl AcpThread {
12321240 connection,
12331241 session_id,
12341242 token_usage : None ,
1243+ cost : None ,
12351244 prompt_capabilities,
12361245 available_commands : Vec :: new ( ) ,
12371246 _observe_prompt_capabilities : task,
@@ -1348,6 +1357,10 @@ impl AcpThread {
13481357 self . token_usage . as_ref ( )
13491358 }
13501359
1360+ pub fn cost ( & self ) -> Option < & SessionCost > {
1361+ self . cost . as_ref ( )
1362+ }
1363+
13511364 pub fn has_pending_edit_tool_calls ( & self ) -> bool {
13521365 for entry in self . entries . iter ( ) . rev ( ) {
13531366 match entry {
@@ -1463,6 +1476,18 @@ impl AcpThread {
14631476 config_options,
14641477 ..
14651478 } ) => cx. emit ( AcpThreadEvent :: ConfigOptionsUpdated ( config_options) ) ,
1479+ acp:: SessionUpdate :: UsageUpdate ( update) if cx. has_flag :: < AcpBetaFeatureFlag > ( ) => {
1480+ let usage = self . token_usage . get_or_insert_with ( Default :: default) ;
1481+ usage. max_tokens = update. size ;
1482+ usage. used_tokens = update. used ;
1483+ if let Some ( cost) = update. cost {
1484+ self . cost = Some ( SessionCost {
1485+ amount : cost. amount ,
1486+ currency : cost. currency . into ( ) ,
1487+ } ) ;
1488+ }
1489+ cx. emit ( AcpThreadEvent :: TokenUsageUpdated ) ;
1490+ }
14661491 _ => { }
14671492 }
14681493 Ok ( ( ) )
@@ -1759,6 +1784,9 @@ impl AcpThread {
17591784 }
17601785
17611786 pub fn update_token_usage ( & mut self , usage : Option < TokenUsage > , cx : & mut Context < Self > ) {
1787+ if usage. is_none ( ) {
1788+ self . cost = None ;
1789+ }
17621790 self . token_usage = usage;
17631791 cx. emit ( AcpThreadEvent :: TokenUsageUpdated ) ;
17641792 }
@@ -2340,6 +2368,15 @@ impl AcpThread {
23402368 }
23412369 }
23422370
2371+ if cx. has_flag :: < AcpBetaFeatureFlag > ( )
2372+ && let Some ( response_usage) = & r. usage
2373+ {
2374+ let usage = this. token_usage . get_or_insert_with ( Default :: default) ;
2375+ usage. input_tokens = response_usage. input_tokens ;
2376+ usage. output_tokens = response_usage. output_tokens ;
2377+ cx. emit ( AcpThreadEvent :: TokenUsageUpdated ) ;
2378+ }
2379+
23432380 cx. emit ( AcpThreadEvent :: Stopped ( r. stop_reason ) ) ;
23442381 Ok ( Some ( r) )
23452382 }
@@ -5297,4 +5334,173 @@ mod tests {
52975334 "session info title update should not propagate back to the connection"
52985335 ) ;
52995336 }
5337+
5338+ #[ gpui:: test]
5339+ async fn test_usage_update_populates_token_usage_and_cost ( cx : & mut TestAppContext ) {
5340+ init_test ( cx) ;
5341+
5342+ let fs = FakeFs :: new ( cx. executor ( ) ) ;
5343+ let project = Project :: test ( fs, [ ] , cx) . await ;
5344+ let connection = Rc :: new ( FakeAgentConnection :: new ( ) ) ;
5345+ let thread = cx
5346+ . update ( |cx| {
5347+ connection. new_session ( project, PathList :: new ( & [ Path :: new ( path ! ( "/test" ) ) ] ) , cx)
5348+ } )
5349+ . await
5350+ . unwrap ( ) ;
5351+
5352+ thread. update ( cx, |thread, cx| {
5353+ thread
5354+ . handle_session_update (
5355+ acp:: SessionUpdate :: UsageUpdate (
5356+ acp:: UsageUpdate :: new ( 5000 , 10000 ) . cost ( acp:: Cost :: new ( 0.42 , "USD" ) ) ,
5357+ ) ,
5358+ cx,
5359+ )
5360+ . unwrap ( ) ;
5361+ } ) ;
5362+
5363+ thread. read_with ( cx, |thread, _| {
5364+ let usage = thread. token_usage ( ) . expect ( "token_usage should be set" ) ;
5365+ assert_eq ! ( usage. max_tokens, 10000 ) ;
5366+ assert_eq ! ( usage. used_tokens, 5000 ) ;
5367+
5368+ let cost = thread. cost ( ) . expect ( "cost should be set" ) ;
5369+ assert ! ( ( cost. amount - 0.42 ) . abs( ) < f64 :: EPSILON ) ;
5370+ assert_eq ! ( cost. currency. as_ref( ) , "USD" ) ;
5371+ } ) ;
5372+ }
5373+
5374+ #[ gpui:: test]
5375+ async fn test_usage_update_without_cost_preserves_existing_cost ( cx : & mut TestAppContext ) {
5376+ init_test ( cx) ;
5377+
5378+ let fs = FakeFs :: new ( cx. executor ( ) ) ;
5379+ let project = Project :: test ( fs, [ ] , cx) . await ;
5380+ let connection = Rc :: new ( FakeAgentConnection :: new ( ) ) ;
5381+ let thread = cx
5382+ . update ( |cx| {
5383+ connection. new_session ( project, PathList :: new ( & [ Path :: new ( path ! ( "/test" ) ) ] ) , cx)
5384+ } )
5385+ . await
5386+ . unwrap ( ) ;
5387+
5388+ thread. update ( cx, |thread, cx| {
5389+ thread
5390+ . handle_session_update (
5391+ acp:: SessionUpdate :: UsageUpdate (
5392+ acp:: UsageUpdate :: new ( 1000 , 10000 ) . cost ( acp:: Cost :: new ( 0.10 , "USD" ) ) ,
5393+ ) ,
5394+ cx,
5395+ )
5396+ . unwrap ( ) ;
5397+
5398+ thread
5399+ . handle_session_update (
5400+ acp:: SessionUpdate :: UsageUpdate ( acp:: UsageUpdate :: new ( 2000 , 10000 ) ) ,
5401+ cx,
5402+ )
5403+ . unwrap ( ) ;
5404+ } ) ;
5405+
5406+ thread. read_with ( cx, |thread, _| {
5407+ let usage = thread. token_usage ( ) . expect ( "token_usage should be set" ) ;
5408+ assert_eq ! ( usage. used_tokens, 2000 ) ;
5409+
5410+ let cost = thread. cost ( ) . expect ( "cost should be preserved" ) ;
5411+ assert ! ( ( cost. amount - 0.10 ) . abs( ) < f64 :: EPSILON ) ;
5412+ } ) ;
5413+ }
5414+
5415+ #[ gpui:: test]
5416+ async fn test_response_usage_does_not_clobber_session_usage ( cx : & mut TestAppContext ) {
5417+ init_test ( cx) ;
5418+
5419+ let fs = FakeFs :: new ( cx. executor ( ) ) ;
5420+ let project = Project :: test ( fs, [ ] , cx) . await ;
5421+ let connection = Rc :: new ( FakeAgentConnection :: new ( ) . on_user_message (
5422+ move |_, thread, mut cx| {
5423+ async move {
5424+ thread. update ( & mut cx, |thread, cx| {
5425+ thread
5426+ . handle_session_update (
5427+ acp:: SessionUpdate :: UsageUpdate (
5428+ acp:: UsageUpdate :: new ( 3000 , 10000 )
5429+ . cost ( acp:: Cost :: new ( 0.05 , "EUR" ) ) ,
5430+ ) ,
5431+ cx,
5432+ )
5433+ . unwrap ( ) ;
5434+ } ) ?;
5435+ Ok ( acp:: PromptResponse :: new ( acp:: StopReason :: EndTurn )
5436+ . usage ( acp:: Usage :: new ( 500 , 200 , 300 ) ) )
5437+ }
5438+ . boxed_local ( )
5439+ } ,
5440+ ) ) ;
5441+
5442+ let thread = cx
5443+ . update ( |cx| {
5444+ connection. new_session ( project, PathList :: new ( & [ Path :: new ( path ! ( "/test" ) ) ] ) , cx)
5445+ } )
5446+ . await
5447+ . unwrap ( ) ;
5448+
5449+ thread
5450+ . update ( cx, |thread, cx| thread. send_raw ( "hello" , cx) )
5451+ . await
5452+ . unwrap ( ) ;
5453+
5454+ thread. read_with ( cx, |thread, _| {
5455+ let usage = thread. token_usage ( ) . expect ( "token_usage should be set" ) ;
5456+ assert_eq ! ( usage. max_tokens, 10000 , "max_tokens from UsageUpdate" ) ;
5457+ assert_eq ! ( usage. used_tokens, 3000 , "used_tokens from UsageUpdate" ) ;
5458+ assert_eq ! ( usage. input_tokens, 200 , "input_tokens from response usage" ) ;
5459+ assert_eq ! (
5460+ usage. output_tokens, 300 ,
5461+ "output_tokens from response usage"
5462+ ) ;
5463+
5464+ let cost = thread. cost ( ) . expect ( "cost should be set" ) ;
5465+ assert ! ( ( cost. amount - 0.05 ) . abs( ) < f64 :: EPSILON ) ;
5466+ assert_eq ! ( cost. currency. as_ref( ) , "EUR" ) ;
5467+ } ) ;
5468+ }
5469+
5470+ #[ gpui:: test]
5471+ async fn test_clearing_token_usage_also_clears_cost ( cx : & mut TestAppContext ) {
5472+ init_test ( cx) ;
5473+
5474+ let fs = FakeFs :: new ( cx. executor ( ) ) ;
5475+ let project = Project :: test ( fs, [ ] , cx) . await ;
5476+ let connection = Rc :: new ( FakeAgentConnection :: new ( ) ) ;
5477+ let thread = cx
5478+ . update ( |cx| {
5479+ connection. new_session ( project, PathList :: new ( & [ Path :: new ( path ! ( "/test" ) ) ] ) , cx)
5480+ } )
5481+ . await
5482+ . unwrap ( ) ;
5483+
5484+ thread. update ( cx, |thread, cx| {
5485+ thread
5486+ . handle_session_update (
5487+ acp:: SessionUpdate :: UsageUpdate (
5488+ acp:: UsageUpdate :: new ( 1000 , 10000 ) . cost ( acp:: Cost :: new ( 0.25 , "USD" ) ) ,
5489+ ) ,
5490+ cx,
5491+ )
5492+ . unwrap ( ) ;
5493+
5494+ assert ! ( thread. token_usage( ) . is_some( ) ) ;
5495+ assert ! ( thread. cost( ) . is_some( ) ) ;
5496+
5497+ thread. update_token_usage ( None , cx) ;
5498+
5499+ assert ! ( thread. token_usage( ) . is_none( ) ) ;
5500+ assert ! (
5501+ thread. cost( ) . is_none( ) ,
5502+ "cost should be cleared when token usage is cleared"
5503+ ) ;
5504+ } ) ;
5505+ }
53005506}
0 commit comments