From 9ce90ff01e61b97903002ade1825e8c4d6e7bc97 Mon Sep 17 00:00:00 2001 From: yunyaozhou Date: Tue, 30 Jun 2026 17:00:33 +0800 Subject: [PATCH] fix: show request timing and nested usage --- codex_retry_gateway_tui.py | 50 +++++++++++++++++++++++--------------- tests/test_payload.py | 28 +++++++++++++++++++++ 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/codex_retry_gateway_tui.py b/codex_retry_gateway_tui.py index a74031b..16ab066 100644 --- a/codex_retry_gateway_tui.py +++ b/codex_retry_gateway_tui.py @@ -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: diff --git a/tests/test_payload.py b/tests/test_payload.py index 077db38..8fe73c5 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -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 = {