@@ -37,6 +37,27 @@ def normalize_to_pil_images(
3737 return [image ]
3838
3939
40+ def _format_duration (seconds : float ) -> str :
41+ """Format a duration given in seconds as ``HH:MM:SS`` or
42+ ``HH:MM:SS.mmm`` for sub-second precision.
43+
44+ Used by `SimpleHtmlReporter` to render both the overall execution time and
45+ per-conversation durations consistently.
46+ """
47+ total_seconds = max (float (seconds ), 0.0 )
48+ whole_seconds = int (total_seconds )
49+ millis = int (round ((total_seconds - whole_seconds ) * 1000 ))
50+ if millis == 1000 :
51+ whole_seconds += 1
52+ millis = 0
53+ hours , remainder = divmod (whole_seconds , 3600 )
54+ minutes , secs = divmod (remainder , 60 )
55+ base = f"{ hours :02d} :{ minutes :02d} :{ secs :02d} "
56+ if whole_seconds == 0 and millis > 0 :
57+ return f"{ base } .{ millis :03d} "
58+ return base
59+
60+
4061def truncate_base64_images (content : Any ) -> Any :
4162 """Replace base64 image data with a placeholder to keep reports readable.
4263
@@ -1010,6 +1031,9 @@ def generate(self) -> None:
10101031 </span>
10111032 <span class="usage-breakdown-meta">
10121033 {{ conversation_usage.step_summaries | length }} step(s),
1034+ {% if conversation_usage.duration_seconds is not none %}
1035+ Duration: {{ format_duration(conversation_usage.duration_seconds) }},
1036+ {% endif %}
10131037 Input {{ "{:,}".format(conversation_usage.input_tokens or 0) }},
10141038 Output {{ "{:,}".format(conversation_usage.output_tokens or 0) }},
10151039 Cache Create {{ "{:,}".format(conversation_usage.cache_creation_input_tokens or 0) }},
@@ -1026,6 +1050,9 @@ def generate(self) -> None:
10261050 <table class="nested-table">
10271051 <tr>
10281052 <th>Conversation ID</th>
1053+ {% if conversation_usage.duration_seconds is not none %}
1054+ <th>Duration</th>
1055+ {% endif %}
10291056 <th>Input Tokens</th>
10301057 <th>Output Tokens</th>
10311058 <th>Cache Create</th>
@@ -1036,6 +1063,9 @@ def generate(self) -> None:
10361063 </tr>
10371064 <tr class="system">
10381065 <td class="mono">{{ conversation_usage.conversation_id }}</td>
1066+ {% if conversation_usage.duration_seconds is not none %}
1067+ <td>{{ format_duration(conversation_usage.duration_seconds) }}</td>
1068+ {% endif %}
10391069 <td>{{ "{:,}".format(conversation_usage.input_tokens or 0) }}</td>
10401070 <td>{{ "{:,}".format(conversation_usage.output_tokens or 0) }}</td>
10411071 <td>{{ "{:,}".format(conversation_usage.cache_creation_input_tokens or 0) }}</td>
@@ -1141,10 +1171,9 @@ def generate(self) -> None:
11411171 end_time = datetime .now (tz = timezone .utc )
11421172 execution_time_formatted : str | None = None
11431173 if self ._start_time is not None :
1144- total_secs = int ((end_time - self ._start_time ).total_seconds ())
1145- hours , remainder = divmod (total_secs , 3600 )
1146- minutes , secs = divmod (remainder , 60 )
1147- execution_time_formatted = f"{ hours :02d} :{ minutes :02d} :{ secs :02d} "
1174+ execution_time_formatted = _format_duration (
1175+ (end_time - self ._start_time ).total_seconds ()
1176+ )
11481177
11491178 html = template .render (
11501179 timestamp = end_time ,
@@ -1153,6 +1182,7 @@ def generate(self) -> None:
11531182 usage_summary = self .usage_summary ,
11541183 cache_original_usage = self .cache_original_usage ,
11551184 execution_time_formatted = execution_time_formatted ,
1185+ format_duration = _format_duration ,
11561186 )
11571187
11581188 report_path = (
0 commit comments