55from django .conf import settings
66from django .db import Error as DatabaseError
77from django .db import IntegrityError
8- from django .db .models import Count , Q
8+ from django .db .models import Count , Q , Sum
99from django .db .models .signals import post_save
1010from django .dispatch import receiver
1111from django .urls import reverse
@@ -93,6 +93,10 @@ def _render_payload(activity: Activity) -> tuple[str, str, dict]:
9393 "trigger_owner" : owner ,
9494 "repo_id" : repo ,
9595 "duration_seconds" : activity .duration ,
96+ "input_tokens" : activity .input_tokens ,
97+ "output_tokens" : activity .output_tokens ,
98+ "total_tokens" : activity .total_tokens ,
99+ "cost_usd" : float (activity .cost_usd ) if activity .cost_usd is not None else None ,
96100 }
97101 return subject , body , context
98102
@@ -155,6 +159,10 @@ def _handle_batch_completion(activity: Activity, siblings, total: int) -> None:
155159 agg = siblings .aggregate (
156160 terminal = Count ("id" , filter = Q (status__in = ActivityStatus .terminal ())),
157161 successful = Count ("id" , filter = Q (status = ActivityStatus .SUCCESSFUL )),
162+ total_input_tokens = Sum ("input_tokens" ),
163+ total_output_tokens = Sum ("output_tokens" ),
164+ total_total_tokens = Sum ("total_tokens" ),
165+ total_cost_usd = Sum ("cost_usd" ),
158166 )
159167 if agg ["terminal" ] < total :
160168 return
@@ -175,12 +183,18 @@ def _handle_batch_completion(activity: Activity, siblings, total: int) -> None:
175183 failed = total - successful
176184 agg_status = ActivityStatus .SUCCESSFUL if failed == 0 else ActivityStatus .FAILED
177185
178- rows = list (siblings .values_list ("repo_id" , "started_at" , "finished_at" ))
186+ rows = list (siblings .values_list ("repo_id" , "started_at" , "finished_at" , "status" ))
179187
180188 effective = activity .effective_notify_on
181189 channels = [cls .channel_type for cls in enabled_channels ()] if _status_matches (effective , agg_status ) else []
182190
183- subject , body , context = _render_batch_payload (activity , rows , total , successful , failed , agg_status )
191+ usage = {
192+ "input_tokens" : agg ["total_input_tokens" ],
193+ "output_tokens" : agg ["total_output_tokens" ],
194+ "total_tokens" : agg ["total_total_tokens" ],
195+ "cost_usd" : float (agg ["total_cost_usd" ]) if agg ["total_cost_usd" ] is not None else None ,
196+ }
197+ subject , body , context = _render_batch_payload (activity , rows , total , successful , failed , agg_status , usage )
184198 link_url = f"{ reverse ('activity_list' )} ?batch={ activity .batch_id } "
185199
186200 for recipient in recipients .values ():
@@ -232,11 +246,14 @@ def _rollup_exists(recipient, batch_id) -> bool:
232246
233247
234248def _render_batch_payload (
235- activity : Activity , rows : list [tuple ], total : int , successful : int , failed : int , agg_status : str
249+ activity : Activity , rows : list [tuple ], total : int , successful : int , failed : int , agg_status : str , usage : dict
236250) -> tuple [str , str , dict ]:
237251 is_schedule = _is_schedule (activity )
238252 ok = failed == 0
239- repo_ids = sorted ({repo for repo , _start , _end in rows if repo })
253+ repo_ids = sorted ({repo for repo , _start , _end , _status in rows if repo })
254+ repo_results = [
255+ {"repo" : repo , "ok" : status == ActivityStatus .SUCCESSFUL } for repo , _start , _end , status in rows if repo
256+ ]
240257 name = activity .scheduled_job .name if is_schedule else ""
241258 owner = str (activity .scheduled_job .user ) if is_schedule else ""
242259
@@ -277,11 +294,16 @@ def _render_batch_payload(
277294 "trigger_owner" : owner ,
278295 "repo_id" : repo_ids [0 ] if len (repo_ids ) == 1 else "" ,
279296 "repo_ids" : repo_ids ,
297+ "repo_results" : repo_results ,
280298 "total" : total ,
281299 "successful_count" : successful ,
282300 "failed_count" : failed ,
283301 "duration_seconds" : _batch_duration (rows ),
284302 "batch_id" : str (activity .batch_id ),
303+ "input_tokens" : usage ["input_tokens" ],
304+ "output_tokens" : usage ["output_tokens" ],
305+ "total_tokens" : usage ["total_tokens" ],
306+ "cost_usd" : usage ["cost_usd" ],
285307 }
286308 return subject , body , context
287309
@@ -297,7 +319,7 @@ def _summarize_repos(repo_ids: list[str], limit: int = 3) -> str:
297319
298320def _batch_duration (rows : list [tuple ]) -> float | None :
299321 """Wall-clock span from earliest start to latest finish across the batch."""
300- pairs = [(start , end ) for _repo , start , end in rows if start and end ]
322+ pairs = [(start , end ) for _repo , start , end , _status in rows if start and end ]
301323 if not pairs :
302324 return None
303325 earliest = min (start for start , _end in pairs )
0 commit comments