feat: show account window balances

This commit is contained in:
2026-08-02 23:58:31 +08:00
parent f757b824c2
commit 845f103bd1
5 changed files with 38 additions and 11 deletions
+7 -2
View File
@@ -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.
+1 -1
View File
@@ -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"
+24 -5
View File
@@ -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 '-'} | "
+5 -2
View File
@@ -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:
Generated
+1 -1
View File
@@ -85,7 +85,7 @@ wheels = [
[[package]]
name = "shusub2"
version = "0.2.12"
version = "0.2.13"
source = { editable = "." }
dependencies = [
{ name = "textual" },