fix: show request timing and nested usage

This commit is contained in:
2026-06-30 17:00:33 +08:00
parent 979dd81c8f
commit 9ce90ff01e
2 changed files with 59 additions and 19 deletions
+31 -19
View File
@@ -310,6 +310,13 @@ def format_ms(value: Any) -> str:
return "-" if number <= 0 else f"{number}ms"
def format_duration_ms_as_seconds(value: Any) -> str:
number = as_float(value)
if number <= 0:
return "-"
return format_elapsed_seconds(number / 1000.0)
def format_bytes(value: Any) -> str:
number = as_int(value)
if number <= 0:
@@ -657,6 +664,7 @@ def normalize_request_rows(payload: dict[str, Any], filter_text: str = "") -> li
continue
if needle and not request_match_score(entry, needle):
continue
usage = entry.get("usage") if isinstance(entry.get("usage"), dict) else {}
rows.append(
{
"seq": as_int(entry.get("seq")),
@@ -675,11 +683,11 @@ def normalize_request_rows(payload: dict[str, Any], filter_text: str = "") -> li
"inspected": bool(entry.get("inspected")),
"status_code": entry.get("status_code"),
"upstream_status_code": entry.get("upstream_status_code"),
"reasoning_tokens": entry.get("reasoning_tokens"),
"input_tokens": entry.get("input_tokens"),
"output_tokens": entry.get("output_tokens"),
"total_tokens": entry.get("total_tokens"),
"cached_tokens": entry.get("cached_tokens"),
"reasoning_tokens": entry.get("reasoning_tokens", usage.get("reasoning_tokens")),
"input_tokens": entry.get("input_tokens", usage.get("input_tokens")),
"output_tokens": entry.get("output_tokens", usage.get("output_tokens")),
"total_tokens": entry.get("total_tokens", usage.get("total_tokens")),
"cached_tokens": entry.get("cached_tokens", usage.get("cached_tokens")),
"upstream_attempt_count": entry.get("upstream_attempt_count"),
"request_body_bytes": entry.get("request_body_bytes"),
"response_bytes_received": entry.get("response_bytes_received"),
@@ -707,18 +715,18 @@ def render_request_detail(row: dict[str, Any]) -> str:
f"seq {row['seq']}",
f"id {primary_request_id(row)}",
f"thread {row.get('thread_id') or '-'}",
f"started {short_time(row['started_at'])}",
f"{row['method']} {row['path']}",
f"profile {row.get('profile_name') or '-'}",
f"status {row['status_code'] or '-'}",
f"upstream {row['upstream_status_code'] or '-'}",
f"attempts {row['upstream_attempt_count'] or 0}",
f"first {format_ms(row['first_response_delay_ms'])}",
f"duration {format_ms(row['duration_ms'])}",
f"first {format_duration_ms_as_seconds(row['first_response_delay_ms'])}",
f"duration {format_duration_ms_as_seconds(row['duration_ms'])}",
f"request {format_bytes(row['request_body_bytes'])}",
f"response {format_bytes(row['response_bytes_received'])}",
f"chunks {row['stream_chunk_count'] or 0}",
f"life {request_lifecycle_label(row)}",
f"started {short_time(row['started_at'])}",
f"finished {short_time(row['finished_at'])}",
f"updated {request_updated_elapsed(row)}",
]
@@ -1144,7 +1152,7 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in
requests = self.query_one("#requests_table", DataTable)
requests.cursor_type = "row"
requests.zebra_stripes = True
requests.add_columns("Seq", "ID", "Thread", "Status", "Path", "Model", "Reasoning", "Usage", "Resp", "Chunks", "First", "Duration", "Updated", "Note")
requests.add_columns("Seq", "ID", "Thread", "Started", "Status", "Path", "Model", "Reasoning", "Usage", "Resp", "Chunks", "First", "Duration", "Updated", "Note")
logs = self.query_one("#logs_table", DataTable)
logs.cursor_type = "row"
@@ -1219,22 +1227,25 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in
table = self.query_one("#requests_table", DataTable)
if table.cursor_row is None or table.cursor_row < 0:
return None
key = str(table.get_row_at(table.cursor_row).key.value)
return self.request_by_key.get(key)
if table.cursor_row >= len(self.requests):
return None
return self.requests[table.cursor_row]
def _selected_profile_row(self) -> dict[str, Any] | None:
table = self.query_one("#profiles_table", DataTable)
if table.cursor_row is None or table.cursor_row < 0:
return None
key = str(table.get_row_at(table.cursor_row).key.value)
return self.profile_by_key.get(key)
if table.cursor_row >= len(self.profiles):
return None
return self.profiles[table.cursor_row]
def _selected_log_row(self) -> dict[str, Any] | None:
table = self.query_one("#logs_table", DataTable)
if table.cursor_row is None or table.cursor_row < 0:
return None
key = str(table.get_row_at(table.cursor_row).key.value)
return self.log_by_key.get(key)
if table.cursor_row >= len(self.logs):
return None
return self.logs[table.cursor_row]
def _set_status(self, text: str) -> None:
self.query_one("#status", Static).update(text)
@@ -1320,6 +1331,7 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in
str(row["seq"]),
short_text(primary_request_id(row), 18) or "-",
short_text(row["thread_id"], 18) or "-",
short_time(row["started_at"]),
f"{status_symbol(row)} {row.get('status_code') or '-'}",
short_text(row["path"], 22),
short_text(row["model"] or row["requested_model"] or row["forwarded_model"], 16),
@@ -1327,8 +1339,8 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in
short_text(request_usage_summary(row), 24),
format_bytes(row["response_bytes_received"]),
request_chunk_progress(row),
format_ms(row["first_response_delay_ms"]),
format_ms(row["duration_ms"]),
format_duration_ms_as_seconds(row["first_response_delay_ms"]),
format_duration_ms_as_seconds(row["duration_ms"]),
request_updated_elapsed(row),
f"{row['upstream_attempt_count'] or 0}x {short_text(row['error'], 18)}",
key=key,
@@ -1573,9 +1585,9 @@ def print_once(snapshot: dict[str, Any], filter_text: str = "") -> None:
for row in snapshot.get("requests") or []:
try:
print(
f"{row['seq']:>6} {short_text(primary_request_id(row), 16):<16} {short_text(row.get('thread_id'), 14):<14} {status_symbol(row)} {row.get('status_code') or '-':<4} "
f"{row['seq']:>6} {short_text(primary_request_id(row), 16):<16} {short_text(row.get('thread_id'), 14):<14} {short_time(row.get('started_at')):<19} {status_symbol(row)} {row.get('status_code') or '-':<4} "
f"{short_text(row['path'], 20):<20} {short_text(row['model'] or row['requested_model'] or row['forwarded_model'], 16):<16} "
f"{short_text(request_usage_summary(row), 26):<26} {request_chunk_progress(row):<14} {format_ms(row['duration_ms']):<8} {request_updated_elapsed(row):<8} "
f"{short_text(request_usage_summary(row), 26):<26} {request_chunk_progress(row):<14} {format_duration_ms_as_seconds(row['duration_ms']):<8} {request_updated_elapsed(row):<8} "
f"{format_count(row['upstream_attempt_count'] or 0):<4} {short_text(row['error'], 30)}"
)
except BrokenPipeError:
+28
View File
@@ -109,6 +109,34 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
}
self.assertEqual(mod.request_usage_summary(row), "in 75 | out 40 | cached 25 (25%)")
def test_request_rows_read_usage_from_nested_usage_object(self) -> None:
mod = load_module()
payload = {
"entries": [
{
"seq": 1,
"request_id": "req_nested",
"path": "/responses",
"usage": {
"input_tokens": 120,
"output_tokens": 33,
"total_tokens": 153,
"cached_tokens": 20,
},
}
]
}
rows = mod.normalize_request_rows(payload)
self.assertEqual(rows[0]["input_tokens"], 120)
self.assertEqual(rows[0]["output_tokens"], 33)
self.assertEqual(rows[0]["total_tokens"], 153)
self.assertEqual(rows[0]["cached_tokens"], 20)
def test_format_duration_ms_as_seconds(self) -> None:
mod = load_module()
self.assertEqual(mod.format_duration_ms_as_seconds(1532), "1.5s")
self.assertEqual(mod.format_duration_ms_as_seconds(120000), "120s")
def test_profile_editor_payload_round_trip(self) -> None:
mod = load_module()
row = {