diff --git a/README.md b/README.md index 293af97..104a5c8 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ complete keys, accounts, and models. On a narrow terminal, a table scrolls horizontally instead of truncating a field: ```text -ACCOUNT | Group | Today | Daily | 5h | 7d | Avail +ACCOUNT | Group | Today | Daily | 5h | 5h Bal | 7d | 7d Bal | Avail KEY | Today | Tokens | Req LOG KEY | Account | Model | First | Duration | Tok/s | Input | Output | Cache | Tokens | Cost | Time | Age ERR | Status | Key | Account | Model | Time | Age @@ -241,7 +241,7 @@ panel after the accounts table when a token is configured. The dedicated Accounts page keeps the full columns for scanning inside zellij: ```text -Name | Provider | Group | Daily | Today | Tokens | Req | Kind | 5h | 7d | Reset | Status | Availability +Name | Provider | Group | Daily | Today | Tokens | Req | Kind | 5h | 5h Bal | 7d | 7d Bal | Reset | Status | Availability ``` `Provider` distinguishes `openai` and `anthropic` accounts from the public @@ -250,6 +250,11 @@ Name | Provider | Group | Daily | Today | Tokens | Req | Kind | 5h | 7d | Reset `Daily` is shown as `used/limit` when Sub2API has `quota_daily_*` fields in `accounts.extra`; otherwise it is `-`. +`5h` / `7d` retain the used/remaining percentages, while `5h Bal` / `7d Bal` show +that same window's derived remaining USD balance from the token-safe account +feed. A missing window stays `-`; the client does not invent a cross-window +total or currency conversion. + `Group` is the derived Sub2API tier alias group. Higher tiers win when multiple aliases exist: `id < slow < fast < sfast`. The table shows `sfast` first, then `fast`, `slow`, `id`, and ungrouped accounts. diff --git a/pyproject.toml b/pyproject.toml index 0a3bff6..c1557b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "shusub2" -version = "0.2.12" +version = "0.2.13" 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 22f803b..104d0a0 100644 --- a/sub2api_quota_tui.py +++ b/sub2api_quota_tui.py @@ -20,7 +20,7 @@ from typing import Any APP_NAME = "shusub2" -FALLBACK_VERSION = "0.2.12" +FALLBACK_VERSION = "0.2.13" 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" @@ -1063,6 +1063,13 @@ def window_cell(account: dict[str, Any], window_id: str) -> str: return f"{format_percent(window.get('used_percent'))}/{format_percent(window.get('remaining_percent'))}" +def window_balance_cell(account: dict[str, Any], window_id: str) -> str: + window = window_for(account, window_id) + if not window or window.get("remaining_balance_usd") is None: + return "-" + return format_cost(window.get("remaining_balance_usd")) + + def daily_quota_cell(account: dict[str, Any]) -> str: limit = account.get("daily_quota_limit") used = account.get("daily_quota_used") @@ -1135,6 +1142,8 @@ def normalize_account_rows(payload: dict[str, Any], filter_text: str = "") -> li "quota_used_percent_max": account.get("quota_used_percent_max"), "five_hour": window_cell(account, "five-hour"), "weekly": window_cell(account, "weekly"), + "five_hour_balance": window_balance_cell(account, "five-hour"), + "weekly_balance": window_balance_cell(account, "weekly"), "reset": reset_cell(account), "latest_usage_at": short_time(account.get("latest_usage_at") or account.get("last_used_at")), "error": str(account.get("error") or ""), @@ -1172,7 +1181,7 @@ def print_once(payload: dict[str, Any], filter_text: str = "", status_payload: d print(summary_line(payload)) if status_payload or status_error: print(monitor_summary(status_payload or {}, status_error)) - print("name provider group daily today tokens req kind 5h 7d reset status availability") + print("name provider group daily today tokens req kind 5h 5h bal 7d 7d bal reset status availability") for row in normalize_account_rows(payload, filter_text): print( f"{row['name'][:30]:<30} " @@ -1184,7 +1193,9 @@ def print_once(payload: dict[str, Any], filter_text: str = "", status_payload: d f"{format_count(row['today_requests']):<5} " f"{row['kind_label']:<9} " f"{row['five_hour']:<9} " + f"{row['five_hour_balance']:<9} " f"{row['weekly']:<9} " + f"{row['weekly_balance']:<9} " f"{row['reset']:<11} " f"{row['status']:<10} " f"{monitor_availability(row, status_payload or {})}" @@ -1274,7 +1285,7 @@ def run_textual( def on_mount(self) -> None: self.configure_table( self.query_one("#accounts", DataTable), - ("ACCOUNT", "Group", "Today", "Daily", "5h", "7d", "Avail"), + ("ACCOUNT", "Group", "Today", "Daily", "5h", "5h Bal", "7d", "7d Bal", "Avail"), ) self.configure_table( self.query_one("#keys", DataTable), @@ -1426,7 +1437,9 @@ def run_textual( format_cost(row["today_cost_usd"]), row["daily_quota_cell"], row["five_hour"], + row["five_hour_balance"], row["weekly"], + row["weekly_balance"], monitor_availability(row, self.status_payload), key=key, ) @@ -1578,7 +1591,10 @@ def run_textual( if table_id == "accounts": detail = ( f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | " - f"daily {row['daily_quota_cell']} | today {format_cost(row['today_cost_usd'])}, " + f"daily {row['daily_quota_cell']} | " + f"5h {row['five_hour_balance']} ({row['five_hour']}) | " + f"7d {row['weekly_balance']} ({row['weekly']}) | " + f"today {format_cost(row['today_cost_usd'])}, " f"{format_count(row['today_tokens'])} tokens, {format_count(row['today_requests'])} req | " f"{monitor_detail(row, self.status_payload)}" ) @@ -1647,7 +1663,7 @@ def run_textual( table = self.query_one("#accounts", DataTable) table.cursor_type = "row" table.zebra_stripes = True - table.add_columns("Name", "Provider", "Group", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status", "Availability") + table.add_columns("Name", "Provider", "Group", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "5h Bal", "7d", "7d Bal", "Reset", "Status", "Availability") keys_table = self.query_one("#keys", DataTable) keys_table.cursor_type = "row" keys_table.zebra_stripes = True @@ -1735,7 +1751,9 @@ def run_textual( format_count(row["today_requests"]), row["kind_label"], row["five_hour"], + row["five_hour_balance"], row["weekly"], + row["weekly_balance"], row["reset"], row["status"], monitor_availability(row, self.status_payload), @@ -1759,6 +1777,7 @@ def run_textual( f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | {row['account_type']} | " f"daily {row['daily_quota_cell']} ({format_percent(row['daily_quota_used_percent'])} used, " f"{row['daily_quota_remaining'] if row['daily_quota_remaining'] is not None else '-'} left) | " + f"5h {row['five_hour_balance']} ({row['five_hour']}) | 7d {row['weekly_balance']} ({row['weekly']}) | " f"today {format_cost(row['today_cost_usd'])}, {format_count(row['today_tokens'])} tokens, " f"{format_count(row['today_requests'])} req | latest {row['latest_usage_at']} | " f"priority {row['priority'] if row['priority'] is not None else '-'} | " diff --git a/tests/test_payload.py b/tests/test_payload.py index d7bcc4d..0d480cd 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -39,8 +39,8 @@ class Sub2APIQuotaTUITests(unittest.TestCase): "today_tokens": 2000, "today_requests": 4, "windows": [ - {"id": "five-hour", "used_percent": 80, "remaining_percent": 20, "reset": "2026-06-09T15:00:00+08:00"}, - {"id": "weekly", "used_percent": 10, "remaining_percent": 90, "reset": "2026-06-10T15:00:00+08:00"}, + {"id": "five-hour", "used_percent": 80, "remaining_percent": 20, "remaining_balance_usd": 4, "reset": "2026-06-09T15:00:00+08:00"}, + {"id": "weekly", "used_percent": 10, "remaining_percent": 90, "remaining_balance_usd": 108, "reset": "2026-06-10T15:00:00+08:00"}, ], }, { @@ -72,6 +72,8 @@ class Sub2APIQuotaTUITests(unittest.TestCase): self.assertEqual(rows[0]["kind_label"], "usage") self.assertEqual(rows[1]["five_hour"], "80%/20%") self.assertEqual(rows[1]["weekly"], "10%/90%") + self.assertEqual(rows[1]["five_hour_balance"], "$4") + self.assertEqual(rows[1]["weekly_balance"], "$108") self.assertIn("today $0.75", mod.summary_line(payload)) def test_filter_matches_kind_and_name(self) -> None: @@ -250,6 +252,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase): self.assertEqual(mod.monitor_availability(rows[0], status_payload), "ok") self.assertIn("availability", out.getvalue()) + self.assertIn("5h bal", out.getvalue()) self.assertIn(" ok\n", out.getvalue()) def test_monitor_availability_is_dash_without_bound_monitor(self) -> None: diff --git a/uv.lock b/uv.lock index be574a0..7372d4a 100644 --- a/uv.lock +++ b/uv.lock @@ -85,7 +85,7 @@ wheels = [ [[package]] name = "shusub2" -version = "0.2.12" +version = "0.2.13" source = { editable = "." } dependencies = [ { name = "textual" },