fix: show request timing and nested usage
This commit is contained in:
+31
-19
@@ -310,6 +310,13 @@ def format_ms(value: Any) -> str:
|
|||||||
return "-" if number <= 0 else f"{number}ms"
|
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:
|
def format_bytes(value: Any) -> str:
|
||||||
number = as_int(value)
|
number = as_int(value)
|
||||||
if number <= 0:
|
if number <= 0:
|
||||||
@@ -657,6 +664,7 @@ def normalize_request_rows(payload: dict[str, Any], filter_text: str = "") -> li
|
|||||||
continue
|
continue
|
||||||
if needle and not request_match_score(entry, needle):
|
if needle and not request_match_score(entry, needle):
|
||||||
continue
|
continue
|
||||||
|
usage = entry.get("usage") if isinstance(entry.get("usage"), dict) else {}
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"seq": as_int(entry.get("seq")),
|
"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")),
|
"inspected": bool(entry.get("inspected")),
|
||||||
"status_code": entry.get("status_code"),
|
"status_code": entry.get("status_code"),
|
||||||
"upstream_status_code": entry.get("upstream_status_code"),
|
"upstream_status_code": entry.get("upstream_status_code"),
|
||||||
"reasoning_tokens": entry.get("reasoning_tokens"),
|
"reasoning_tokens": entry.get("reasoning_tokens", usage.get("reasoning_tokens")),
|
||||||
"input_tokens": entry.get("input_tokens"),
|
"input_tokens": entry.get("input_tokens", usage.get("input_tokens")),
|
||||||
"output_tokens": entry.get("output_tokens"),
|
"output_tokens": entry.get("output_tokens", usage.get("output_tokens")),
|
||||||
"total_tokens": entry.get("total_tokens"),
|
"total_tokens": entry.get("total_tokens", usage.get("total_tokens")),
|
||||||
"cached_tokens": entry.get("cached_tokens"),
|
"cached_tokens": entry.get("cached_tokens", usage.get("cached_tokens")),
|
||||||
"upstream_attempt_count": entry.get("upstream_attempt_count"),
|
"upstream_attempt_count": entry.get("upstream_attempt_count"),
|
||||||
"request_body_bytes": entry.get("request_body_bytes"),
|
"request_body_bytes": entry.get("request_body_bytes"),
|
||||||
"response_bytes_received": entry.get("response_bytes_received"),
|
"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"seq {row['seq']}",
|
||||||
f"id {primary_request_id(row)}",
|
f"id {primary_request_id(row)}",
|
||||||
f"thread {row.get('thread_id') or '-'}",
|
f"thread {row.get('thread_id') or '-'}",
|
||||||
|
f"started {short_time(row['started_at'])}",
|
||||||
f"{row['method']} {row['path']}",
|
f"{row['method']} {row['path']}",
|
||||||
f"profile {row.get('profile_name') or '-'}",
|
f"profile {row.get('profile_name') or '-'}",
|
||||||
f"status {row['status_code'] or '-'}",
|
f"status {row['status_code'] or '-'}",
|
||||||
f"upstream {row['upstream_status_code'] or '-'}",
|
f"upstream {row['upstream_status_code'] or '-'}",
|
||||||
f"attempts {row['upstream_attempt_count'] or 0}",
|
f"attempts {row['upstream_attempt_count'] or 0}",
|
||||||
f"first {format_ms(row['first_response_delay_ms'])}",
|
f"first {format_duration_ms_as_seconds(row['first_response_delay_ms'])}",
|
||||||
f"duration {format_ms(row['duration_ms'])}",
|
f"duration {format_duration_ms_as_seconds(row['duration_ms'])}",
|
||||||
f"request {format_bytes(row['request_body_bytes'])}",
|
f"request {format_bytes(row['request_body_bytes'])}",
|
||||||
f"response {format_bytes(row['response_bytes_received'])}",
|
f"response {format_bytes(row['response_bytes_received'])}",
|
||||||
f"chunks {row['stream_chunk_count'] or 0}",
|
f"chunks {row['stream_chunk_count'] or 0}",
|
||||||
f"life {request_lifecycle_label(row)}",
|
f"life {request_lifecycle_label(row)}",
|
||||||
f"started {short_time(row['started_at'])}",
|
|
||||||
f"finished {short_time(row['finished_at'])}",
|
f"finished {short_time(row['finished_at'])}",
|
||||||
f"updated {request_updated_elapsed(row)}",
|
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 = self.query_one("#requests_table", DataTable)
|
||||||
requests.cursor_type = "row"
|
requests.cursor_type = "row"
|
||||||
requests.zebra_stripes = True
|
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 = self.query_one("#logs_table", DataTable)
|
||||||
logs.cursor_type = "row"
|
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)
|
table = self.query_one("#requests_table", DataTable)
|
||||||
if table.cursor_row is None or table.cursor_row < 0:
|
if table.cursor_row is None or table.cursor_row < 0:
|
||||||
return None
|
return None
|
||||||
key = str(table.get_row_at(table.cursor_row).key.value)
|
if table.cursor_row >= len(self.requests):
|
||||||
return self.request_by_key.get(key)
|
return None
|
||||||
|
return self.requests[table.cursor_row]
|
||||||
|
|
||||||
def _selected_profile_row(self) -> dict[str, Any] | None:
|
def _selected_profile_row(self) -> dict[str, Any] | None:
|
||||||
table = self.query_one("#profiles_table", DataTable)
|
table = self.query_one("#profiles_table", DataTable)
|
||||||
if table.cursor_row is None or table.cursor_row < 0:
|
if table.cursor_row is None or table.cursor_row < 0:
|
||||||
return None
|
return None
|
||||||
key = str(table.get_row_at(table.cursor_row).key.value)
|
if table.cursor_row >= len(self.profiles):
|
||||||
return self.profile_by_key.get(key)
|
return None
|
||||||
|
return self.profiles[table.cursor_row]
|
||||||
|
|
||||||
def _selected_log_row(self) -> dict[str, Any] | None:
|
def _selected_log_row(self) -> dict[str, Any] | None:
|
||||||
table = self.query_one("#logs_table", DataTable)
|
table = self.query_one("#logs_table", DataTable)
|
||||||
if table.cursor_row is None or table.cursor_row < 0:
|
if table.cursor_row is None or table.cursor_row < 0:
|
||||||
return None
|
return None
|
||||||
key = str(table.get_row_at(table.cursor_row).key.value)
|
if table.cursor_row >= len(self.logs):
|
||||||
return self.log_by_key.get(key)
|
return None
|
||||||
|
return self.logs[table.cursor_row]
|
||||||
|
|
||||||
def _set_status(self, text: str) -> None:
|
def _set_status(self, text: str) -> None:
|
||||||
self.query_one("#status", Static).update(text)
|
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"]),
|
str(row["seq"]),
|
||||||
short_text(primary_request_id(row), 18) or "-",
|
short_text(primary_request_id(row), 18) or "-",
|
||||||
short_text(row["thread_id"], 18) or "-",
|
short_text(row["thread_id"], 18) or "-",
|
||||||
|
short_time(row["started_at"]),
|
||||||
f"{status_symbol(row)} {row.get('status_code') or '-'}",
|
f"{status_symbol(row)} {row.get('status_code') or '-'}",
|
||||||
short_text(row["path"], 22),
|
short_text(row["path"], 22),
|
||||||
short_text(row["model"] or row["requested_model"] or row["forwarded_model"], 16),
|
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),
|
short_text(request_usage_summary(row), 24),
|
||||||
format_bytes(row["response_bytes_received"]),
|
format_bytes(row["response_bytes_received"]),
|
||||||
request_chunk_progress(row),
|
request_chunk_progress(row),
|
||||||
format_ms(row["first_response_delay_ms"]),
|
format_duration_ms_as_seconds(row["first_response_delay_ms"]),
|
||||||
format_ms(row["duration_ms"]),
|
format_duration_ms_as_seconds(row["duration_ms"]),
|
||||||
request_updated_elapsed(row),
|
request_updated_elapsed(row),
|
||||||
f"{row['upstream_attempt_count'] or 0}x {short_text(row['error'], 18)}",
|
f"{row['upstream_attempt_count'] or 0}x {short_text(row['error'], 18)}",
|
||||||
key=key,
|
key=key,
|
||||||
@@ -1573,9 +1585,9 @@ def print_once(snapshot: dict[str, Any], filter_text: str = "") -> None:
|
|||||||
for row in snapshot.get("requests") or []:
|
for row in snapshot.get("requests") or []:
|
||||||
try:
|
try:
|
||||||
print(
|
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(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)}"
|
f"{format_count(row['upstream_attempt_count'] or 0):<4} {short_text(row['error'], 30)}"
|
||||||
)
|
)
|
||||||
except BrokenPipeError:
|
except BrokenPipeError:
|
||||||
|
|||||||
@@ -109,6 +109,34 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
self.assertEqual(mod.request_usage_summary(row), "in 75 | out 40 | cached 25 (25%)")
|
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:
|
def test_profile_editor_payload_round_trip(self) -> None:
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
row = {
|
row = {
|
||||||
|
|||||||
Reference in New Issue
Block a user