feat: show account window balances
This commit is contained in:
@@ -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
@@ -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
@@ -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 '-'} | "
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user