feat: per-key today usage panel under the accounts table
- accounts page gains a #keys DataTable below the accounts table showing each API key's usage today: Key | Today | Tokens | Req, sorted by cost - data comes from the Sub2API admin dashboard api-keys-trend (requests, tokens, key names) plus api-keys-usage (today_actual_cost), derived from the configured logs url and reusing the same admin API key - key names use the same stable per-key colors as the logs page - --once prints the same panel after the accounts snapshot; failures or a missing token surface in the status line without touching accounts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -563,5 +563,84 @@ class Sub2APILogsTests(unittest.TestCase):
|
||||
os.environ["SHUSUB2_LOGS_URL_FILE"] = old_file
|
||||
|
||||
|
||||
def sample_key_usage_payload() -> dict:
|
||||
return {
|
||||
"date": "2026-07-21",
|
||||
"trend": [
|
||||
{"date": "2026-07-21", "api_key_id": 3, "key_name": "codex-main", "requests": 120, "tokens": 4_500_000},
|
||||
{"date": "2026-07-21", "api_key_id": 4, "key_name": "claude-max", "requests": 40, "tokens": 9_100_000},
|
||||
{"date": "2026-07-21", "api_key_id": 9, "key_name": "test", "requests": 3, "tokens": 250},
|
||||
],
|
||||
"stats": {
|
||||
"3": {"api_key_id": 3, "today_actual_cost": 12.5, "total_actual_cost": 100.0},
|
||||
"4": {"api_key_id": 4, "today_actual_cost": 30.25, "total_actual_cost": 90.0},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Sub2APIKeyUsageTests(unittest.TestCase):
|
||||
def test_admin_api_base_derived_from_logs_url(self) -> None:
|
||||
mod = load_module()
|
||||
|
||||
self.assertEqual(
|
||||
mod.admin_api_base("https://sub2apicn.shujk.top/api/v1/admin/usage"),
|
||||
"https://sub2apicn.shujk.top/api/v1/admin",
|
||||
)
|
||||
self.assertEqual(
|
||||
mod.admin_api_base("https://sub2apicn.shujk.top/api/v1/admin/usage/"),
|
||||
"https://sub2apicn.shujk.top/api/v1/admin",
|
||||
)
|
||||
self.assertEqual(
|
||||
mod.admin_api_base("https://sub2apicn.shujk.top/api/v1/admin"),
|
||||
"https://sub2apicn.shujk.top/api/v1/admin",
|
||||
)
|
||||
self.assertEqual(mod.admin_api_base("https://example.com/other/path"), "")
|
||||
self.assertEqual(mod.admin_api_base("not a url"), "")
|
||||
|
||||
def test_normalize_key_rows_merges_costs_and_sorts_by_cost(self) -> None:
|
||||
mod = load_module()
|
||||
|
||||
rows = mod.normalize_key_rows(sample_key_usage_payload())
|
||||
|
||||
self.assertEqual([row["name"] for row in rows], ["claude-max", "codex-main", "test"])
|
||||
top = rows[0]
|
||||
self.assertEqual(top["cost"], 30.25)
|
||||
self.assertEqual(top["tokens"], 9_100_000)
|
||||
self.assertEqual(top["requests"], 40)
|
||||
# key 9 has no cost stats -> cost 0, sorted last
|
||||
self.assertEqual(rows[-1]["cost"], 0.0)
|
||||
self.assertEqual(rows[-1]["requests"], 3)
|
||||
|
||||
def test_normalize_key_rows_aggregates_multiple_points_per_key(self) -> None:
|
||||
mod = load_module()
|
||||
payload = {
|
||||
"trend": [
|
||||
{"api_key_id": 3, "key_name": "codex-main", "requests": 10, "tokens": 100},
|
||||
{"api_key_id": 3, "key_name": "codex-main", "requests": 5, "tokens": 50},
|
||||
],
|
||||
"stats": {},
|
||||
}
|
||||
|
||||
rows = mod.normalize_key_rows(payload)
|
||||
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]["requests"], 15)
|
||||
self.assertEqual(rows[0]["tokens"], 150)
|
||||
|
||||
def test_print_keys_once_renders_key_summary(self) -> None:
|
||||
mod = load_module()
|
||||
out = io.StringIO()
|
||||
|
||||
with contextlib.redirect_stdout(out):
|
||||
mod.print_keys_once(sample_key_usage_payload())
|
||||
|
||||
text = out.getvalue()
|
||||
self.assertIn("keys today 2026-07-21 | 3 keys | $42.75", text)
|
||||
self.assertIn("claude-max", text)
|
||||
self.assertIn("$30.25", text)
|
||||
self.assertIn("9.1M", text)
|
||||
self.assertIn("codex-main", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user