diff --git a/README.md b/README.md index 027a8fe..a505e04 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Sub2API admin usage API and refreshes every 60 seconds by default (`--logs-refresh-seconds` / `SHUSUB2_LOGS_REFRESH_SECONDS`). Columns: ```text -Key | Account | Model | Type | Tokens | Cost | Latency | First | Tok/s | Time +Key | Account | Model | Effort | Type | Tokens | Cost | First | Latency | Tok/s | Time ``` `Type` is the Sub2API `request_type` (`sync` / `stream` / `ws_v2` / `cyber`). @@ -91,8 +91,10 @@ Key | Account | Model | Type | Tokens | Cost | Latency | First | Tok/s | Time the table shows the per-bucket breakdown, actual cost, first-token latency, decode speed, upstream model mapping, user, and request id. -`First` is the first-token latency (`first_token_ms`). `Tok/s` is the decode -throughput computed as `output_tokens / (latency - first_token)`; it shows +`Effort` is the request's `reasoning_effort` (`-` when absent). `First` is the +first-token latency and `Latency` the total duration, both shown in seconds. +`Tok/s` is the decode throughput computed as +`output_tokens / (latency - first_token)`; it shows `-` when there is no output or no positive decode window. In the TUI each API key name is rendered in a stable per-key color so rows from the same key are easy to group visually (`--once --logs` output stays plain text). diff --git a/pyproject.toml b/pyproject.toml index 2607bba..71e1f37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "shusub2" -version = "0.2.1" +version = "0.2.2" description = "Terminal UI for Sub2API account quota and daily usage" readme = "README.md" requires-python = ">=3.11" diff --git a/sub2api_quota_tui.py b/sub2api_quota_tui.py index 3caca5c..b11522b 100644 --- a/sub2api_quota_tui.py +++ b/sub2api_quota_tui.py @@ -18,7 +18,7 @@ from typing import Any APP_NAME = "shusub2" -FALLBACK_VERSION = "0.2.1" +FALLBACK_VERSION = "0.2.2" DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts" DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url" DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url" @@ -369,6 +369,13 @@ def format_rate(value: Any) -> str: return f"{rate:.1f}/s" +def format_seconds(value: Any) -> str: + number = as_int(value) + if number <= 0: + return "-" + return f"{number / 1000:.1f}s" + + KEY_COLOR_PALETTE = ( "cyan", "magenta", @@ -413,6 +420,7 @@ def normalize_log_rows(payload: dict[str, Any], filter_text: str = "") -> list[d user_name = nested_name(item, "user", "user_id", "user_name") model = str(item.get("model") or "-") upstream_model = str(item.get("upstream_model") or "").strip() + effort = str(item.get("reasoning_effort") or "").strip() or "-" type_label = log_type_label(item) row = { "id": as_int(item.get("id")), @@ -421,6 +429,7 @@ def normalize_log_rows(payload: dict[str, Any], filter_text: str = "") -> list[d "user": user_name, "model": model, "upstream_model": upstream_model, + "effort": effort, "type": type_label, "tokens": log_total_tokens(item), "input_tokens": as_int(item.get("input_tokens")), @@ -439,7 +448,7 @@ def normalize_log_rows(payload: dict[str, Any], filter_text: str = "") -> list[d } if needle: haystack = " ".join( - (key_name, account_name, user_name, model, upstream_model, type_label, row["request_id"]) + (key_name, account_name, user_name, model, upstream_model, effort, type_label, row["request_id"]) ).lower() if needle not in haystack: continue @@ -465,12 +474,14 @@ def log_detail_line(row: dict[str, Any]) -> str: model = row["model"] if row["upstream_model"] and row["upstream_model"] != row["model"]: model = f"{row['model']} -> {row['upstream_model']}" + if row["effort"] != "-": + model = f"{model} ({row['effort']})" detail = ( f"{row['time']} | key {row['key']} | account {row['account']} | user {row['user']} | {model} | {row['type']} | " f"tokens in {format_count(row['input_tokens'])} out {format_count(row['output_tokens'])} " f"cache_w {format_count(row['cache_creation_tokens'])} cache_r {format_count(row['cache_read_tokens'])} | " f"cost {format_cost(row['cost'])} (actual {format_cost(row['actual_cost'])}) | " - f"latency {format_latency(row['duration_ms'])} first {format_latency(row['first_token_ms'])} " + f"first {format_seconds(row['first_token_ms'])} latency {format_seconds(row['duration_ms'])} " f"{format_rate(row['tokens_per_second'])}" ) if row["request_id"]: @@ -481,17 +492,18 @@ def log_detail_line(row: dict[str, Any]) -> str: def print_logs_once(payload: dict[str, Any], filter_text: str = "") -> None: rows = normalize_log_rows(payload, filter_text) print(logs_summary_line(payload, len(rows))) - print("key account model type tokens cost latency first tok/s time") + print("key account model effort type tokens cost first latency tok/s time") for row in rows: print( f"{row['key'][:20]:<21} " f"{row['account'][:20]:<21} " f"{row['model'][:24]:<25} " + f"{row['effort']:<7} " f"{row['type']:<7} " f"{format_count(row['tokens']):<8} " f"{format_cost(row['cost']):<10} " - f"{format_latency(row['duration_ms']):<9} " - f"{format_latency(row['first_token_ms']):<9} " + f"{format_seconds(row['first_token_ms']):<8} " + f"{format_seconds(row['duration_ms']):<8} " f"{format_rate(row['tokens_per_second']):<8} " f"{row['time']}" ) @@ -927,7 +939,7 @@ def run_textual( table = self.query_one("#logs", DataTable) table.cursor_type = "row" table.zebra_stripes = True - table.add_columns("Key", "Account", "Model", "Type", "Tokens", "Cost", "Latency", "First", "Tok/s", "Time") + table.add_columns("Key", "Account", "Model", "Effort", "Type", "Tokens", "Cost", "First", "Latency", "Tok/s", "Time") self.refresh_data() self.set_interval(logs_refresh_seconds, self.refresh_data) @@ -976,11 +988,12 @@ def run_textual( Text(row["key"], style=color) if color else row["key"], row["account"], row["model"], + row["effort"], row["type"], format_count(row["tokens"]), format_cost(row["cost"]), - format_latency(row["duration_ms"]), - format_latency(row["first_token_ms"]), + format_seconds(row["first_token_ms"]), + format_seconds(row["duration_ms"]), format_rate(row["tokens_per_second"]), row["time"], key=key, diff --git a/tests/test_payload.py b/tests/test_payload.py index a59a9cd..3e1d9e6 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -341,6 +341,7 @@ def sample_logs_payload() -> dict: "user": {"id": 1, "name": "shujakuin"}, "model": "gpt-5.5", "upstream_model": "gpt-5.5-codex", + "reasoning_effort": "high", "request_type": "stream", "stream": True, "input_tokens": 1200, @@ -454,8 +455,21 @@ class Sub2APILogsTests(unittest.TestCase): oldest = rows[-1] self.assertEqual(oldest["first_token_ms"], 800) self.assertAlmostEqual(oldest["tokens_per_second"], 340 * 1000.0 / 4521) - self.assertIn("first 800ms", mod.log_detail_line(oldest)) - self.assertIn("75.2/s", mod.log_detail_line(oldest)) + self.assertEqual(oldest["effort"], "high") + self.assertEqual(rows[0]["effort"], "-") + detail = mod.log_detail_line(oldest) + self.assertIn("first 0.8s latency 5.3s", detail) + self.assertIn("(high)", detail) + self.assertIn("75.2/s", detail) + + def test_format_seconds_converts_ms(self) -> None: + mod = load_module() + + self.assertEqual(mod.format_seconds(5321), "5.3s") + self.assertEqual(mod.format_seconds(800), "0.8s") + self.assertEqual(mod.format_seconds(200735), "200.7s") + self.assertEqual(mod.format_seconds(0), "-") + self.assertEqual(mod.format_seconds(None), "-") def test_key_color_is_stable_and_from_palette(self) -> None: mod = load_module() @@ -483,10 +497,14 @@ class Sub2APILogsTests(unittest.TestCase): self.assertIn("stream", text) self.assertIn("5.7K", text) self.assertIn("$0.012", text) - self.assertIn("5321ms", text) - self.assertIn("first", text) - self.assertIn("tok/s", text) - self.assertIn("800ms", text) + header = text.splitlines()[1] + self.assertLess(header.index("model"), header.index("effort")) + self.assertLess(header.index("effort"), header.index("type")) + self.assertLess(header.index("first"), header.index("latency")) + self.assertIn("tok/s", header) + self.assertIn("high", text) + self.assertIn("5.3s", text) + self.assertIn("0.8s", text) self.assertIn("75.2/s", text) self.assertIn("total 2.3K records", text) diff --git a/uv.lock b/uv.lock index ddf8034..6535808 100644 --- a/uv.lock +++ b/uv.lock @@ -85,7 +85,7 @@ wheels = [ [[package]] name = "shusub2" -version = "0.2.1" +version = "0.2.2" source = { editable = "." } dependencies = [ { name = "textual" },