feat: show req and resp ids in tui
This commit is contained in:
+23
-13
@@ -22,7 +22,7 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
APP_NAME = "codex-retry-gateway-tui"
|
APP_NAME = "codex-retry-gateway-tui"
|
||||||
FALLBACK_VERSION = "0.1.0"
|
FALLBACK_VERSION = "0.1.4"
|
||||||
DEFAULT_GATEWAY_ADMIN_PATH = "/__codex_retry_gateway"
|
DEFAULT_GATEWAY_ADMIN_PATH = "/__codex_retry_gateway"
|
||||||
DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway"
|
DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway"
|
||||||
DEFAULT_API_URL = DEFAULT_GATEWAY_URL
|
DEFAULT_API_URL = DEFAULT_GATEWAY_URL
|
||||||
@@ -386,8 +386,12 @@ def effective_input_tokens(input_tokens: Any, cached_tokens: Any) -> int | None:
|
|||||||
return max(0, total - cached)
|
return max(0, total - cached)
|
||||||
|
|
||||||
|
|
||||||
def primary_request_id(row: dict[str, Any]) -> str:
|
def request_id_text(row: dict[str, Any]) -> str:
|
||||||
return str(row.get("response_id") or row.get("request_id") or "").strip() or "-"
|
return str(row.get("request_id") or "").strip() or "-"
|
||||||
|
|
||||||
|
|
||||||
|
def response_id_text(row: dict[str, Any]) -> str:
|
||||||
|
return str(row.get("response_id") or "").strip() or "-"
|
||||||
|
|
||||||
|
|
||||||
def request_usage_summary(row: dict[str, Any]) -> str:
|
def request_usage_summary(row: dict[str, Any]) -> str:
|
||||||
@@ -474,7 +478,8 @@ def request_row_cells(row: dict[str, Any], text_limits: dict[str, int] | None =
|
|||||||
note_width = max(4, limits.get("note", 22) - len(note_prefix))
|
note_width = max(4, limits.get("note", 22) - len(note_prefix))
|
||||||
return (
|
return (
|
||||||
str(row["seq"]),
|
str(row["seq"]),
|
||||||
short_text(primary_request_id(row), limits.get("id", 18)) or "-",
|
short_text(request_id_text(row), limits.get("req_id", 18)) or "-",
|
||||||
|
short_text(response_id_text(row), limits.get("resp_id", 18)) or "-",
|
||||||
short_text(row["thread_id"], limits.get("thread", 18)) or "-",
|
short_text(row["thread_id"], limits.get("thread", 18)) or "-",
|
||||||
short_time(row["started_at"]),
|
short_time(row["started_at"]),
|
||||||
f"{status_symbol(row)} {row.get('status_code') or '-'}",
|
f"{status_symbol(row)} {row.get('status_code') or '-'}",
|
||||||
@@ -776,7 +781,8 @@ def normalize_request_rows(payload: dict[str, Any], filter_text: str = "") -> li
|
|||||||
def render_request_detail(row: dict[str, Any]) -> str:
|
def render_request_detail(row: dict[str, Any]) -> str:
|
||||||
bits = [
|
bits = [
|
||||||
f"seq {row['seq']}",
|
f"seq {row['seq']}",
|
||||||
f"id {primary_request_id(row)}",
|
f"req {request_id_text(row)}",
|
||||||
|
f"resp {response_id_text(row)}",
|
||||||
f"thread {row.get('thread_id') or '-'}",
|
f"thread {row.get('thread_id') or '-'}",
|
||||||
f"started {short_time(row['started_at'])}",
|
f"started {short_time(row['started_at'])}",
|
||||||
f"{row['method']} {row['path']}",
|
f"{row['method']} {row['path']}",
|
||||||
@@ -1214,7 +1220,8 @@ def run_textual(
|
|||||||
self.request_table_width_profiles = {
|
self.request_table_width_profiles = {
|
||||||
"compact": {
|
"compact": {
|
||||||
"seq": 7,
|
"seq": 7,
|
||||||
"id": 18,
|
"req_id": 18,
|
||||||
|
"resp_id": 18,
|
||||||
"thread": 18,
|
"thread": 18,
|
||||||
"started": 19,
|
"started": 19,
|
||||||
"status": 7,
|
"status": 7,
|
||||||
@@ -1232,7 +1239,8 @@ def run_textual(
|
|||||||
},
|
},
|
||||||
"wide": {
|
"wide": {
|
||||||
"seq": 7,
|
"seq": 7,
|
||||||
"id": 36,
|
"req_id": 24,
|
||||||
|
"resp_id": 24,
|
||||||
"thread": 36,
|
"thread": 36,
|
||||||
"started": 19,
|
"started": 19,
|
||||||
"status": 7,
|
"status": 7,
|
||||||
@@ -1272,7 +1280,8 @@ def run_textual(
|
|||||||
width_profile = self.request_table_width_profiles[self.request_table_density]
|
width_profile = self.request_table_width_profiles[self.request_table_density]
|
||||||
self.request_table_column_keys = {
|
self.request_table_column_keys = {
|
||||||
"seq": requests.add_column("Seq", width=width_profile["seq"], key="seq"),
|
"seq": requests.add_column("Seq", width=width_profile["seq"], key="seq"),
|
||||||
"id": requests.add_column("ID", width=width_profile["id"], key="id"),
|
"req_id": requests.add_column("Req ID", width=width_profile["req_id"], key="req_id"),
|
||||||
|
"resp_id": requests.add_column("Resp ID", width=width_profile["resp_id"], key="resp_id"),
|
||||||
"thread": requests.add_column("Thread", width=width_profile["thread"], key="thread"),
|
"thread": requests.add_column("Thread", width=width_profile["thread"], key="thread"),
|
||||||
"started": requests.add_column("Started", width=width_profile["started"], key="started"),
|
"started": requests.add_column("Started", width=width_profile["started"], key="started"),
|
||||||
"status": requests.add_column("Status", width=width_profile["status"], key="status"),
|
"status": requests.add_column("Status", width=width_profile["status"], key="status"),
|
||||||
@@ -1280,8 +1289,8 @@ def run_textual(
|
|||||||
"model": requests.add_column("Model", width=width_profile["model"], key="model"),
|
"model": requests.add_column("Model", width=width_profile["model"], key="model"),
|
||||||
"reasoning": requests.add_column("Reasoning", width=width_profile["reasoning"], key="reasoning"),
|
"reasoning": requests.add_column("Reasoning", width=width_profile["reasoning"], key="reasoning"),
|
||||||
"usage": requests.add_column("Usage", width=self.request_usage_width, key="usage"),
|
"usage": requests.add_column("Usage", width=self.request_usage_width, key="usage"),
|
||||||
"req": requests.add_column("Req", width=width_profile["req"], key="req"),
|
"req_bytes": requests.add_column("Req Size", width=width_profile["req"], key="req_bytes"),
|
||||||
"resp": requests.add_column("Resp", width=width_profile["resp"], key="resp"),
|
"resp_bytes": requests.add_column("Resp Size", width=width_profile["resp"], key="resp_bytes"),
|
||||||
"chunks": requests.add_column("Chunks", width=width_profile["chunks"], key="chunks"),
|
"chunks": requests.add_column("Chunks", width=width_profile["chunks"], key="chunks"),
|
||||||
"first": requests.add_column("First", width=width_profile["first"], key="first"),
|
"first": requests.add_column("First", width=width_profile["first"], key="first"),
|
||||||
"duration": requests.add_column("Duration", width=width_profile["duration"], key="duration"),
|
"duration": requests.add_column("Duration", width=width_profile["duration"], key="duration"),
|
||||||
@@ -1559,14 +1568,15 @@ def run_textual(
|
|||||||
def _request_text_limits(self, table: DataTable) -> dict[str, int]:
|
def _request_text_limits(self, table: DataTable) -> dict[str, int]:
|
||||||
width_profile = self.request_table_width_profiles[self.request_table_density]
|
width_profile = self.request_table_width_profiles[self.request_table_density]
|
||||||
limits = {
|
limits = {
|
||||||
"id": width_profile["id"],
|
"req_id": width_profile["req_id"],
|
||||||
|
"resp_id": width_profile["resp_id"],
|
||||||
"thread": width_profile["thread"],
|
"thread": width_profile["thread"],
|
||||||
"path": width_profile["path"],
|
"path": width_profile["path"],
|
||||||
"model": width_profile["model"],
|
"model": width_profile["model"],
|
||||||
"usage": self.request_usage_width,
|
"usage": self.request_usage_width,
|
||||||
"note": width_profile["note"],
|
"note": width_profile["note"],
|
||||||
}
|
}
|
||||||
for name in ("id", "thread", "path", "model", "usage", "note"):
|
for name in ("req_id", "resp_id", "thread", "path", "model", "usage", "note"):
|
||||||
key = self.request_table_column_keys.get(name)
|
key = self.request_table_column_keys.get(name)
|
||||||
column = table.columns.get(key) if key is not None else None
|
column = table.columns.get(key) if key is not None else None
|
||||||
if column is None or not getattr(column, "width", 0):
|
if column is None or not getattr(column, "width", 0):
|
||||||
@@ -1791,7 +1801,7 @@ 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} {short_time(row.get('started_at')):<19} {status_symbol(row)} {row.get('status_code') or '-':<4} "
|
f"{row['seq']:>6} {short_text(request_id_text(row), 16):<16} {short_text(response_id_text(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} {format_bytes(row.get('request_body_bytes')):<8} {request_chunk_progress(row):<14} {format_duration_ms_as_seconds(row['duration_ms']):<8} {request_updated_elapsed(row):<8} "
|
f"{short_text(request_usage_summary(row), 26):<26} {format_bytes(row.get('request_body_bytes')):<8} {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)}"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "codex-retry-gateway-tui"
|
name = "codex-retry-gateway-tui"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
description = "Terminal UI for codex-retry-gateway monitoring and control"
|
description = "Terminal UI for codex-retry-gateway monitoring and control"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
+16
-10
@@ -94,8 +94,11 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
|
|||||||
"finished_at": "2026-06-30T00:00:01Z",
|
"finished_at": "2026-06-30T00:00:01Z",
|
||||||
"usage_last_updated_at": "2026-06-30T00:00:01Z",
|
"usage_last_updated_at": "2026-06-30T00:00:01Z",
|
||||||
}
|
}
|
||||||
self.assertEqual(mod.primary_request_id(row), "resp_1")
|
self.assertEqual(mod.request_id_text(row), "r")
|
||||||
|
self.assertEqual(mod.response_id_text(row), "resp_1")
|
||||||
self.assertIn("thread thread_1", mod.render_request_detail(row))
|
self.assertIn("thread thread_1", mod.render_request_detail(row))
|
||||||
|
self.assertIn("req r", mod.render_request_detail(row))
|
||||||
|
self.assertIn("resp resp_1", mod.render_request_detail(row))
|
||||||
self.assertIn("updated 1.0s", mod.render_request_detail(row))
|
self.assertIn("updated 1.0s", mod.render_request_detail(row))
|
||||||
self.assertEqual(mod.request_updated_elapsed(row), "1.0s")
|
self.assertEqual(mod.request_updated_elapsed(row), "1.0s")
|
||||||
self.assertEqual(mod.request_chunk_progress(row), "7 / 4.1KB")
|
self.assertEqual(mod.request_chunk_progress(row), "7 / 4.1KB")
|
||||||
@@ -136,15 +139,16 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
|
|||||||
"error": "Selected model is at capacity. Please try a different model.",
|
"error": "Selected model is at capacity. Please try a different model.",
|
||||||
"finished_at": "2026-06-30T12:25:00Z",
|
"finished_at": "2026-06-30T12:25:00Z",
|
||||||
}
|
}
|
||||||
compact = mod.request_row_cells(row, {"id": 18, "thread": 18, "path": 22, "model": 16, "usage": 24, "note": 22})
|
compact = mod.request_row_cells(row, {"req_id": 18, "resp_id": 18, "thread": 18, "path": 22, "model": 16, "usage": 24, "note": 22})
|
||||||
wide = mod.request_row_cells(row, {"id": 36, "thread": 36, "path": 40, "model": 24, "usage": 72, "note": 48})
|
wide = mod.request_row_cells(row, {"req_id": 36, "resp_id": 36, "thread": 36, "path": 40, "model": 24, "usage": 72, "note": 48})
|
||||||
self.assertEqual(compact[9], "0B")
|
self.assertEqual(compact[10], "0B")
|
||||||
self.assertEqual(wide[9], "0B")
|
self.assertEqual(wide[10], "0B")
|
||||||
self.assertLess(len(compact[1]), len(wide[1]))
|
self.assertLess(len(compact[1]), len(wide[1]))
|
||||||
self.assertLess(len(compact[2]), len(wide[2]))
|
self.assertLess(len(compact[2]), len(wide[2]))
|
||||||
self.assertLess(len(compact[5]), len(wide[5]))
|
self.assertLess(len(compact[3]), len(wide[3]))
|
||||||
self.assertLess(len(compact[8]), len(wide[8]))
|
self.assertLess(len(compact[6]), len(wide[6]))
|
||||||
self.assertLess(len(compact[15]), len(wide[15]))
|
self.assertLess(len(compact[9]), len(wide[9]))
|
||||||
|
self.assertLess(len(compact[16]), len(wide[16]))
|
||||||
|
|
||||||
def test_request_row_cells_show_request_bytes(self) -> None:
|
def test_request_row_cells_show_request_bytes(self) -> None:
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
@@ -174,8 +178,10 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
|
|||||||
"finished_at": "2026-06-30T12:00:03Z",
|
"finished_at": "2026-06-30T12:00:03Z",
|
||||||
}
|
}
|
||||||
cells = mod.request_row_cells(row)
|
cells = mod.request_row_cells(row)
|
||||||
self.assertEqual(cells[9], "1.5KB")
|
self.assertEqual(cells[1], "req_1")
|
||||||
self.assertEqual(cells[10], "4.1KB")
|
self.assertEqual(cells[2], "resp_1")
|
||||||
|
self.assertEqual(cells[10], "1.5KB")
|
||||||
|
self.assertEqual(cells[11], "4.1KB")
|
||||||
|
|
||||||
def test_request_rows_read_usage_from_nested_usage_object(self) -> None:
|
def test_request_rows_read_usage_from_nested_usage_object(self) -> None:
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ requires-python = ">=3.11"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codex-retry-gateway-tui"
|
name = "codex-retry-gateway-tui"
|
||||||
version = "0.1.0"
|
version = "0.1.4"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "textual" },
|
{ name = "textual" },
|
||||||
|
|||||||
Reference in New Issue
Block a user