feat: show canonical account source and groups

This commit is contained in:
2026-08-08 07:19:51 +08:00
parent 68d37a1584
commit be1c321cd8
5 changed files with 210 additions and 73 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ refresh。组件失败时服务端保留对应 last-good 数据,并在 `compon
Workspace 当前提供以下有界 projection Workspace 当前提供以下有界 projection
- `accounts`:按完整 `(account_id, account_name)` 对齐 server6/server4 fresh binding 和 scheduled-test plan 的账号摘要、quota window 与当日用量;`active_state``schedulable_state``yes` / `partial` / `no``last_test_state` 另可为 `NA`。每个节点先选择自身最新的有效测试结果,聚合 `last_test_time` / `last_test_model` 取两者中较早的一次;详情保留脱敏 `node_status` 用于定位 partial。当前 server6 upstream key-group binding 精确关联时仍包含 `account_rate_multiplier` 原始有效倍率和 `account_rate_multiplier_cny` 展示倍率。 - `accounts`:按完整 `(account_id, account_name)` 对齐 server6/server4 fresh binding 和 scheduled-test plan 的账号摘要、quota window 与当日用量;`active_state``schedulable_state``yes` / `partial` / `no``last_test_state` 另可为 `NA`。每个节点先选择自身最新的有效测试结果,聚合 `last_test_time` / `last_test_model` 取两者中较早的一次;详情保留脱敏 `node_status` 用于定位 partial。当前 server6 upstream key-group binding 精确关联时仍包含 `account_rate_multiplier` 原始有效倍率和 `account_rate_multiplier_cny` 展示倍率,并输出 canonical `pricing_source` / `pricing_collector_source`、对应 source 的 `pricing_balance_cny`、native `account_group_names``pricing_last_fetch_at`。客户端账户表的最后一列显示去重后的全部 native group 名称,`Last fetch` 显示 key/rate binding collector 时间;source、group 或 fetch 不可用时保持 `-` / failure 状态
- `status`channel monitor 摘要。 - `status`channel monitor 摘要。
- `sources`Pricing Monitor 已有的脱敏 source 余额与健康状态。 - `sources`Pricing Monitor 已有的脱敏 source 余额与健康状态。
- `traffic.requests`:仅 server6 的最新有界请求样本;每行带对应 account 的当前 `account_rate_multiplier` / `account_rate_multiplier_cny`,无法稳定关联时为 `null` - `traffic.requests`:仅 server6 的最新有界请求样本;每行带对应 account 的当前 `account_rate_multiplier` / `account_rate_multiplier_cny`,无法稳定关联时为 `null`
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "shusub2" name = "shusub2"
version = "0.3.6" version = "0.3.7"
description = "Aggregated operations TUI for Sub2API" description = "Aggregated operations TUI for Sub2API"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
+104 -27
View File
@@ -27,7 +27,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
APP_NAME = "shusub2" APP_NAME = "shusub2"
FALLBACK_VERSION = "0.3.6" FALLBACK_VERSION = "0.3.7"
DEFAULT_WORKSPACE_URL = "https://price.tailbeb9ad.ts.net/api/ui-data?view=workspace" DEFAULT_WORKSPACE_URL = "https://price.tailbeb9ad.ts.net/api/ui-data?view=workspace"
DEFAULT_WORKSPACE_URL_CONFIG_FILE = "~/.config/shusub2/workspace-url" DEFAULT_WORKSPACE_URL_CONFIG_FILE = "~/.config/shusub2/workspace-url"
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts" DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
@@ -99,7 +99,7 @@ MONITOR_OK_STATUSES = {"operational", "ok", "success"}
MONITOR_FAILED_STATUSES = {"error", "failed", "failure"} MONITOR_FAILED_STATUSES = {"error", "failed", "failure"}
MONITOR_STOPWORDS = {"response", "responses", "monitor"} MONITOR_STOPWORDS = {"response", "responses", "monitor"}
PRICING_ACCOUNT_SOURCE_RE = re.compile( PRICING_ACCOUNT_SOURCE_RE = re.compile(
r"^[a-z0-9]+-quota(?:only)?-(?P<source>[a-z0-9]+(?:-[a-z0-9]+)*)$", r"^[a-z0-9]+-quota-(?P<source>[a-z0-9]+)-(?P<alias>[a-z0-9_]+)$",
re.IGNORECASE, re.IGNORECASE,
) )
ERROR_DISPLAY_LABEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 ._/+\-]{0,95}$") ERROR_DISPLAY_LABEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 ._/+\-]{0,95}$")
@@ -672,6 +672,26 @@ def workspace_account_contract_is_valid(rows: list[dict[str, Any]]) -> bool:
return False return False
if str(row.get("last_test_state") or "") not in {"yes", "partial", "no", "NA"}: if str(row.get("last_test_state") or "") not in {"yes", "partial", "no", "NA"}:
return False return False
for field in (
"pricing_source",
"pricing_collector_source",
"pricing_source_kind",
"pricing_fetch_status",
):
if field in row and not isinstance(row.get(field), str):
return False
group_names = row.get("account_group_names")
if group_names is not None and (
not isinstance(group_names, list)
or len(group_names) > 16
or any(not isinstance(name, str) for name in group_names)
):
return False
fetch_at = row.get("pricing_last_fetch_at")
if fetch_at not in (None, "") and (
not isinstance(fetch_at, str) or parse_time(fetch_at) is None
):
return False
if not isinstance(row.get("last_test_time"), str) or not isinstance( if not isinstance(row.get("last_test_time"), str) or not isinstance(
row.get("last_test_model"), str row.get("last_test_model"), str
): ):
@@ -1783,14 +1803,24 @@ def normalize_account_rows(
for account in payload.get("accounts") or []: for account in payload.get("accounts") or []:
if not isinstance(account, dict): if not isinstance(account, dict):
continue continue
pricing_source = pricing_source_for_account(account.get("name"), pricing_sources) explicit_source_name = str(
account.get("pricing_source") or account.get("pricing_collector_source") or ""
).strip()
pricing_source = (
pricing_sources.get(pricing_source_key(explicit_source_name))
if explicit_source_name
else pricing_source_for_account(account.get("name"), pricing_sources)
)
groups = account_group_display(account)
haystack = " ".join( haystack = " ".join(
str(account.get(key) or "") str(account.get(key) or "")
for key in ( for key in (
"id", "id",
"name", "name",
"routing_group", "routing_group",
"account_group_names",
"provider", "provider",
"pricing_source",
"kind", "kind",
"status", "status",
"account_type", "account_type",
@@ -1809,10 +1839,27 @@ def normalize_account_rows(
"id": as_int(account.get("id")), "id": as_int(account.get("id")),
"name": str(account.get("name") or ""), "name": str(account.get("name") or ""),
"routing_group": routing_group, "routing_group": routing_group,
"groups": groups,
"base_url_hash": str(account.get("base_url_hash") or ""), "base_url_hash": str(account.get("base_url_hash") or ""),
"pricing_source": str(pricing_source.get("name") or "-") if pricing_source else "-", "pricing_source": (
"pricing_cny": pricing_source.get("balance_cny") if pricing_source and pricing_source.get("status") == "healthy" else None, explicit_source_name
"pricing_state": str(pricing_source.get("status") or "-") if pricing_source else "-", or (str(pricing_source.get("name") or "-") if pricing_source else "-")
),
"pricing_cny": (
optional_number(account.get("pricing_balance_cny"))
if explicit_source_name and "pricing_balance_cny" in account
else (
pricing_source.get("balance_cny")
if pricing_source and pricing_source.get("status") == "healthy"
else None
)
),
"pricing_state": (
str(account.get("pricing_fetch_status") or "-")
if explicit_source_name
else (str(pricing_source.get("status") or "-") if pricing_source else "-")
),
"pricing_fetch": account_pricing_fetch_label(account, pricing_source),
"pricing_balance": pricing_source.get("balance") if pricing_source else None, "pricing_balance": pricing_source.get("balance") if pricing_source else None,
"pricing_unit": str(pricing_source.get("unit") or "-") if pricing_source else "-", "pricing_unit": str(pricing_source.get("unit") or "-") if pricing_source else "-",
"pricing_updated": str(pricing_source.get("updated") or "-") if pricing_source else "-", "pricing_updated": str(pricing_source.get("updated") or "-") if pricing_source else "-",
@@ -1905,7 +1952,7 @@ def normalize_pricing_rows(payload: dict[str, Any], filter_text: str = "") -> li
def pricing_source_key(value: Any) -> str: def pricing_source_key(value: Any) -> str:
return re.sub(r"[^a-z0-9]+", "", str(value or "").strip().casefold()) return str(value or "").strip().casefold()
def pricing_source_lookup(payload: dict[str, Any]) -> dict[str, dict[str, Any] | None]: def pricing_source_lookup(payload: dict[str, Any]) -> dict[str, dict[str, Any] | None]:
@@ -1930,6 +1977,32 @@ def pricing_source_for_account(
return pricing_sources.get(pricing_source_key(match.group("source"))) return pricing_sources.get(pricing_source_key(match.group("source")))
def account_group_display(account: dict[str, Any]) -> str:
raw_names = account.get("account_group_names")
if isinstance(raw_names, list):
names: list[str] = []
for raw_name in raw_names[:16]:
name = str(raw_name or "").strip()
if name and name not in names:
names.append(name)
if names:
return "".join(names)
fallback = str(account.get("routing_group") or "").strip()
return fallback or "-"
def account_pricing_fetch_label(
account: dict[str, Any], pricing_source: dict[str, Any] | None
) -> str:
fetched = short_time(account.get("pricing_last_fetch_at"))
status = str(account.get("pricing_fetch_status") or "").strip()
if fetched == "-":
return str(pricing_source.get("updated") or "-") if pricing_source else (status or "-")
if status in {"", "matched", "reused_primary", "ok"}:
return fetched
return f"{fetched} {status}"
def pricing_summary(payload: dict[str, Any], rows: list[dict[str, Any]] | None = None) -> str: def pricing_summary(payload: dict[str, Any], rows: list[dict[str, Any]] | None = None) -> str:
source_rows = rows if rows is not None else normalize_pricing_rows(payload) source_rows = rows if rows is not None else normalize_pricing_rows(payload)
healthy = sum(row["status"] == "healthy" for row in source_rows) healthy = sum(row["status"] == "healthy" for row in source_rows)
@@ -1961,9 +2034,10 @@ def account_pricing_detail(row: dict[str, Any]) -> str:
cny = format_cny(row.get("pricing_cny")) cny = format_cny(row.get("pricing_cny"))
raw_balance = format_amount(row.get("pricing_balance")) raw_balance = format_amount(row.get("pricing_balance"))
unit = str(row.get("pricing_unit") or "-") unit = str(row.get("pricing_unit") or "-")
state = str(row.get("pricing_state") or "-") return (
updated = str(row.get("pricing_updated") or "-") f"source {source} | CNY {cny} | raw {raw_balance} {unit} | "
return f"source {source} | CNY {cny} | raw {raw_balance} {unit} | {state} | updated {updated}" f"last fetch {row.get('pricing_fetch') or '-'}"
)
def print_pricing_once(payload: dict[str, Any], filter_text: str = "") -> None: def print_pricing_once(payload: dict[str, Any], filter_text: str = "") -> None:
@@ -2011,16 +2085,15 @@ def print_once(
print(f"sources: {pricing_error}") print(f"sources: {pricing_error}")
elif pricing_payload: elif pricing_payload:
print(pricing_summary(pricing_payload)) print(pricing_summary(pricing_payload))
print("name provider group source multiplier src cny src state daily today tokens req kind 5h 7d reset status active schedulable last-test last-test-time last-test-model availability") print("name provider source multiplier src cny last fetch daily today tokens req kind 5h 7d reset status active schedulable last-test last-test-time last-test-model availability groups")
for row in normalize_account_rows(payload, filter_text, pricing_payload): for row in normalize_account_rows(payload, filter_text, pricing_payload):
print( print(
f"{row['name'][:30]:<30} " f"{row['name'][:30]:<30} "
f"{row['provider']:<9} " f"{row['provider']:<9} "
f"{row['routing_group']:<6} "
f"{row['pricing_source']:<16} " f"{row['pricing_source']:<16} "
f"{format_multiplier(row['account_multiplier']):<11} " f"{format_multiplier(row['account_multiplier']):<11} "
f"{format_cny(row['pricing_cny']):<13} " f"{format_cny(row['pricing_cny']):<13} "
f"{row['pricing_state']:<12} " f"{row['pricing_fetch']:<21} "
f"{row['daily_quota_cell']:<10} " f"{row['daily_quota_cell']:<10} "
f"{format_cost(row['today_cost_usd']):<10} " f"{format_cost(row['today_cost_usd']):<10} "
f"{format_count(row['today_tokens']):<8} " f"{format_count(row['today_tokens']):<8} "
@@ -2035,7 +2108,8 @@ def print_once(
f"{row['last_test']:<10} " f"{row['last_test']:<10} "
f"{row['last_test_time']:<13} " f"{row['last_test_time']:<13} "
f"{row['last_test_model'][:23]:<23} " f"{row['last_test_model'][:23]:<23} "
f"{monitor_availability(row, status_payload or {})}" f"{monitor_availability(row, status_payload or {}):<12} "
f"{row['groups']:<28}"
) )
@@ -2167,16 +2241,16 @@ def run_textual(
# Content-sized columns preserve complete values on wide terminals; # Content-sized columns preserve complete values on wide terminals;
# narrow terminals use DataTable's horizontal scrolling instead. # narrow terminals use DataTable's horizontal scrolling instead.
for label in columns: for label in columns:
table.add_column(label) table.add_column(label, width=32 if label == "Groups" else None)
def on_mount(self) -> None: def on_mount(self) -> None:
self.app.sub_title = "Dashboard" self.app.sub_title = "Dashboard"
account_columns = ( account_columns = (
"ACCOUNT", "Group", "Source", "Multiplier", "Src CNY", "Src state", "Today", "Daily", "5h", "7d" "ACCOUNT", "Source", "Multiplier", "Src CNY", "Last fetch", "Today", "Daily", "5h", "7d"
) )
if not legacy_direct: if not legacy_direct:
account_columns += ("Active", "Schedulable", "Last test", "Last test time", "Last test model") account_columns += ("Active", "Schedulable", "Last test", "Last test time", "Last test model")
account_columns += ("Avail",) account_columns += ("Avail", "Groups")
self.configure_table( self.configure_table(
self.query_one("#accounts", DataTable), self.query_one("#accounts", DataTable),
account_columns, account_columns,
@@ -2341,11 +2415,10 @@ def run_textual(
self.account_by_key[key] = row self.account_by_key[key] = row
cells: list[Any] = [ cells: list[Any] = [
row["name"], row["name"],
row["routing_group"],
row["pricing_source"], row["pricing_source"],
format_multiplier(row["account_multiplier"]), format_multiplier(row["account_multiplier"]),
format_cny(row["pricing_cny"]), format_cny(row["pricing_cny"]),
row["pricing_state"], row["pricing_fetch"],
format_cost(row["today_cost_usd"]), format_cost(row["today_cost_usd"]),
row["daily_quota_cell"], row["daily_quota_cell"],
row["five_hour"], row["five_hour"],
@@ -2362,6 +2435,7 @@ def run_textual(
) )
) )
cells.append(monitor_availability(row, self.status_payload)) cells.append(monitor_availability(row, self.status_payload))
cells.append(row["groups"])
table.add_row(*cells, key=key) table.add_row(*cells, key=key)
def render_keys(self) -> None: def render_keys(self) -> None:
@@ -2531,7 +2605,7 @@ def run_textual(
def render_detail(self, table_id: str, row: dict[str, Any]) -> None: def render_detail(self, table_id: str, row: dict[str, Any]) -> None:
if table_id == "accounts": if table_id == "accounts":
detail = ( detail = (
f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | " f"{row['name']} | {row['provider']} | groups {row['groups']} | {row['kind_label']} | "
f"multiplier {format_multiplier(row['account_multiplier'])} | " f"multiplier {format_multiplier(row['account_multiplier'])} | "
f"daily {row['daily_quota_cell']} | " f"daily {row['daily_quota_cell']} | "
f"5h {row['five_hour']} | " f"5h {row['five_hour']} | "
@@ -2618,12 +2692,13 @@ def run_textual(
table.cursor_type = "row" table.cursor_type = "row"
table.zebra_stripes = True table.zebra_stripes = True
account_columns = [ account_columns = [
"Name", "Provider", "Group", "Source", "Multiplier", "Src CNY", "Src state", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status" "Name", "Provider", "Source", "Multiplier", "Src CNY", "Last fetch", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status"
] ]
if not legacy_direct: if not legacy_direct:
account_columns.extend(("Active", "Schedulable", "Last test", "Last test time", "Last test model")) account_columns.extend(("Active", "Schedulable", "Last test", "Last test time", "Last test model"))
account_columns.append("Availability") account_columns.extend(("Availability", "Groups"))
table.add_columns(*account_columns) for label in account_columns:
table.add_column(label, width=32 if label == "Groups" else None)
keys_table = self.query_one("#keys", DataTable) keys_table = self.query_one("#keys", DataTable)
keys_table.cursor_type = "row" keys_table.cursor_type = "row"
keys_table.zebra_stripes = True keys_table.zebra_stripes = True
@@ -2719,11 +2794,10 @@ def run_textual(
cells: list[Any] = [ cells: list[Any] = [
row["name"], row["name"],
row["provider"], row["provider"],
row["routing_group"],
row["pricing_source"], row["pricing_source"],
format_multiplier(row["account_multiplier"]), format_multiplier(row["account_multiplier"]),
format_cny(row["pricing_cny"]), format_cny(row["pricing_cny"]),
row["pricing_state"], row["pricing_fetch"],
row["daily_quota_cell"], row["daily_quota_cell"],
format_cost(row["today_cost_usd"]), format_cost(row["today_cost_usd"]),
format_count(row["today_tokens"]), format_count(row["today_tokens"]),
@@ -2744,7 +2818,10 @@ def run_textual(
row["last_test_model"], row["last_test_model"],
) )
) )
cells.append(monitor_availability(row, self.status_payload)) cells.extend((
monitor_availability(row, self.status_payload),
row["groups"],
))
table.add_row(*cells, key=key) table.add_row(*cells, key=key)
self.query_one("#summary", Static).update(summary_line(self.payload)) self.query_one("#summary", Static).update(summary_line(self.payload))
if self.rows: if self.rows:
@@ -2761,7 +2838,7 @@ def run_textual(
def render_detail(self, row: dict[str, Any]) -> None: def render_detail(self, row: dict[str, Any]) -> None:
raw = row.get("raw") if isinstance(row.get("raw"), dict) else {} raw = row.get("raw") if isinstance(row.get("raw"), dict) else {}
detail = ( detail = (
f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | {row['account_type']} | " f"{row['name']} | {row['provider']} | groups {row['groups']} | {row['kind_label']} | {row['account_type']} | "
f"multiplier {format_multiplier(row['account_multiplier'])} | " f"multiplier {format_multiplier(row['account_multiplier'])} | "
f"daily {row['daily_quota_cell']} ({format_percent(row['daily_quota_used_percent'])} used, " 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"{row['daily_quota_remaining'] if row['daily_quota_remaining'] is not None else '-'} left) | "
+103 -43
View File
@@ -43,7 +43,7 @@ def workspace_payload_fixture() -> dict[str, object]:
}, },
"sources": [ "sources": [
{ {
"name": "code-plan", "name": "codeplan",
"source_kind": "newapi", "source_kind": "newapi",
"health_state": "healthy", "health_state": "healthy",
"balance_available": True, "balance_available": True,
@@ -62,7 +62,7 @@ def workspace_payload_fixture() -> dict[str, object]:
"accounts": [ "accounts": [
{ {
"id": "9007199254740993", "id": "9007199254740993",
"name": "oai-quota-code-plan", "name": "oai-quota-codeplan-primary",
"routing_group": "fast", "routing_group": "fast",
"provider": "openai", "provider": "openai",
"kind": "quota_limited", "kind": "quota_limited",
@@ -98,7 +98,7 @@ def workspace_payload_fixture() -> dict[str, object]:
"items": [ "items": [
{ {
"id": "9007199254740995", "id": "9007199254740995",
"name": "oai-quota-code-plan", "name": "oai-quota-codeplan-primary",
"provider": "openai", "provider": "openai",
"latest_status": "success", "latest_status": "success",
} }
@@ -127,7 +127,7 @@ def workspace_payload_fixture() -> dict[str, object]:
"api_key_id": "7", "api_key_id": "7",
"api_key_name": "wmy", "api_key_name": "wmy",
"account_id": "9", "account_id": "9",
"account_name": "oai-quota-code-plan", "account_name": "oai-quota-codeplan-primary",
"account_rate_multiplier": 0.06, "account_rate_multiplier": 0.06,
"account_rate_multiplier_cny": 0.003, "account_rate_multiplier_cny": 0.003,
"model": "gpt-5.5", "model": "gpt-5.5",
@@ -149,7 +149,7 @@ def workspace_payload_fixture() -> dict[str, object]:
"api_key_id": "7", "api_key_id": "7",
"api_key_name": "wmy", "api_key_name": "wmy",
"account_id": "9", "account_id": "9",
"account_name": "oai-quota-code-plan", "account_name": "oai-quota-codeplan-primary",
"model": "gpt-5.5", "model": "gpt-5.5",
"error_type": "api_error", "error_type": "api_error",
"error_source": "upstream_http", "error_source": "upstream_http",
@@ -245,19 +245,19 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
mod = load_module() mod = load_module()
accounts_payload = { accounts_payload = {
"accounts": [ "accounts": [
{"id": 1, "name": "oai-quota-code-plan", "routing_group": "fast", "kind": "quota_limited"}, {"id": 1, "name": "oai-quota-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"},
{"id": 2, "name": "oai-quotaonly-code-plan", "routing_group": "fast", "kind": "quota_limited"}, {"id": 2, "name": "ordinary-account", "routing_group": "fast", "kind": "quota_limited"},
{"id": 3, "name": "oai-sub-code-plan", "routing_group": "fast", "kind": "quota_limited"}, {"id": 3, "name": "oai-sub-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"},
{"id": 4, "name": "oai-quota-unknown", "routing_group": "fast", "kind": "quota_limited"}, {"id": 4, "name": "oai-quota-unknown", "routing_group": "fast", "kind": "quota_limited"},
{"id": 5, "name": "oai-quota-ycy", "routing_group": "fast", "kind": "quota_limited"}, {"id": 5, "name": "oai-quota-ycy-primary", "routing_group": "fast", "kind": "quota_limited"},
{"id": 6, "name": "oai-quota-code--plan", "routing_group": "fast", "kind": "quota_limited"}, {"id": 6, "name": "oai-quota-codeplan--primary", "routing_group": "fast", "kind": "quota_limited"},
{"id": 7, "name": "oai-quota-code-plan-", "routing_group": "fast", "kind": "quota_limited"}, {"id": 7, "name": "oai-quota-codeplan-primary-", "routing_group": "fast", "kind": "quota_limited"},
] ]
} }
pricing_payload = { pricing_payload = {
"sources": [ "sources": [
{ {
"name": "code-plan", "name": "codeplan",
"source_kind": "newapi", "source_kind": "newapi",
"health_state": "healthy", "health_state": "healthy",
"last_success_at": "2026-08-02T12:00:00Z", "last_success_at": "2026-08-02T12:00:00Z",
@@ -275,20 +275,72 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
rows = {row["name"]: row for row in mod.normalize_account_rows(accounts_payload, pricing_payload=pricing_payload)} rows = {row["name"]: row for row in mod.normalize_account_rows(accounts_payload, pricing_payload=pricing_payload)}
self.assertEqual(rows["oai-quota-code-plan"]["pricing_source"], "code-plan") self.assertEqual(rows["oai-quota-codeplan-primary"]["pricing_source"], "codeplan")
self.assertEqual(rows["oai-quota-code-plan"]["pricing_cny"], 10) self.assertEqual(rows["oai-quota-codeplan-primary"]["pricing_cny"], 10)
self.assertEqual(rows["oai-quotaonly-code-plan"]["pricing_source"], "code-plan") self.assertEqual(rows["ordinary-account"]["pricing_source"], "-")
self.assertEqual(rows["oai-sub-code-plan"]["pricing_source"], "-") self.assertEqual(rows["oai-sub-codeplan-primary"]["pricing_source"], "-")
self.assertEqual(rows["oai-quota-unknown"]["pricing_source"], "-") self.assertEqual(rows["oai-quota-unknown"]["pricing_source"], "-")
self.assertEqual(rows["oai-quota-ycy"]["pricing_source"], "ycy") self.assertEqual(rows["oai-quota-ycy-primary"]["pricing_source"], "ycy")
self.assertEqual(rows["oai-quota-ycy"]["pricing_state"], "error") self.assertEqual(rows["oai-quota-ycy-primary"]["pricing_state"], "error")
self.assertIsNone(rows["oai-quota-ycy"]["pricing_cny"]) self.assertIsNone(rows["oai-quota-ycy-primary"]["pricing_cny"])
self.assertEqual(rows["oai-quota-code--plan"]["pricing_source"], "-") self.assertEqual(rows["oai-quota-codeplan--primary"]["pricing_source"], "-")
self.assertEqual(rows["oai-quota-code-plan-"]["pricing_source"], "-") self.assertEqual(rows["oai-quota-codeplan-primary-"]["pricing_source"], "-")
ambiguous = dict(pricing_payload, sources=[*pricing_payload["sources"], {"name": "codeplan", "health_state": "healthy", "balance": {"available_cny": 12}}]) ambiguous = dict(pricing_payload, sources=[*pricing_payload["sources"], {"name": "codeplan", "health_state": "healthy", "balance": {"available_cny": 12}}])
ambiguous_rows = {row["name"]: row for row in mod.normalize_account_rows(accounts_payload, pricing_payload=ambiguous)} ambiguous_rows = {row["name"]: row for row in mod.normalize_account_rows(accounts_payload, pricing_payload=ambiguous)}
self.assertEqual(ambiguous_rows["oai-quota-code-plan"]["pricing_source"], "-") self.assertEqual(ambiguous_rows["oai-quota-codeplan-primary"]["pricing_source"], "-")
def test_explicit_account_source_balance_groups_and_fetch_metadata_win_over_name(self) -> None:
mod = load_module()
accounts_payload = {
"accounts": [
{
"id": 1,
"name": "oai-quota-codeplan-primary",
"routing_group": "legacy-key-group",
"pricing_source": "canonical-provider",
"pricing_collector_source": "canonical-provider",
"pricing_balance_cny": 12.5,
"pricing_last_fetch_at": "2026-08-03T11:59:00Z",
"pricing_fetch_status": "matched",
"account_group_names": ["GPT", "quota", "GPT"],
"kind": "quota_limited",
},
{
"id": 2,
"name": "ordinary-failed",
"pricing_source": "canonical-provider",
"pricing_balance_cny": None,
"pricing_last_fetch_at": "2026-08-03T12:00:00Z",
"pricing_fetch_status": "upstream_error",
"account_group_names": [],
"kind": "quota_limited",
},
]
}
pricing_payload = {
"sources": [
{
"name": "codeplan",
"health_state": "healthy",
"balance": {"available_cny": 99},
}
]
}
rows = {
row["name"]: row
for row in mod.normalize_account_rows(accounts_payload, pricing_payload=pricing_payload)
}
canonical = rows["oai-quota-codeplan-primary"]
self.assertEqual(canonical["pricing_source"], "canonical-provider")
self.assertEqual(canonical["pricing_cny"], 12.5)
self.assertEqual(canonical["groups"], "GPT、quota")
self.assertEqual(canonical["pricing_fetch"], "08-03 19:59")
failed = rows["ordinary-failed"]
self.assertIsNone(failed["pricing_cny"])
self.assertIn("upstream_error", failed["pricing_fetch"])
def test_normalize_pricing_rows_uses_pricing_monitor_source_balances(self) -> None: def test_normalize_pricing_rows_uses_pricing_monitor_source_balances(self) -> None:
mod = load_module() mod = load_module()
@@ -296,7 +348,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
"generated_at": "2026-08-02T12:00:00Z", "generated_at": "2026-08-02T12:00:00Z",
"sources": [ "sources": [
{ {
"name": "code-plan", "name": "codeplan",
"source_kind": "newapi", "source_kind": "newapi",
"health_state": "healthy", "health_state": "healthy",
"last_success_at": "2026-08-02T11:59:00Z", "last_success_at": "2026-08-02T11:59:00Z",
@@ -316,11 +368,13 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
rows = mod.normalize_pricing_rows(payload) rows = mod.normalize_pricing_rows(payload)
self.assertEqual([row["name"] for row in rows], ["code-plan", "kedaya"]) self.assertEqual([row["name"] for row in rows], ["codeplan", "kedaya"])
self.assertEqual(rows[0]["balance"], 5000000) self.assertEqual(rows[0]["balance"], 5000000)
self.assertEqual(rows[0]["balance_cny"], 10) self.assertEqual(rows[0]["balance_cny"], 10)
self.assertEqual(rows[0]["unit"], "quota") self.assertEqual(rows[0]["unit"], "quota")
self.assertEqual(rows[0]["status"], "healthy") self.assertEqual(rows[0]["status"], "healthy")
self.assertEqual(rows[0]["updated_at"], "2026-08-02T11:59:00Z")
self.assertNotEqual(rows[0]["age"], "-")
self.assertEqual(rows[1]["status"], "error") self.assertEqual(rows[1]["status"], "error")
self.assertIsNone(rows[1]["balance"]) self.assertIsNone(rows[1]["balance"])
self.assertIsNone(rows[1]["balance_cny"]) self.assertIsNone(rows[1]["balance_cny"])
@@ -345,7 +399,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
out = io.StringIO() out = io.StringIO()
with contextlib.redirect_stdout(out): with contextlib.redirect_stdout(out):
mod.print_pricing_once(payload) mod.print_pricing_once(payload)
self.assertIn("code-plan", out.getvalue()) self.assertIn("codeplan", out.getvalue())
self.assertIn("5,000,000", out.getvalue()) self.assertIn("5,000,000", out.getvalue())
self.assertIn("¥10", out.getvalue()) self.assertIn("¥10", out.getvalue())
@@ -386,7 +440,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
"view": "accounts", "view": "accounts",
"sources": [ "sources": [
{ {
"name": "code-plan", "name": "codeplan",
"source_kind": "newapi", "source_kind": "newapi",
"health_state": "healthy", "health_state": "healthy",
"balance_available": True, "balance_available": True,
@@ -403,7 +457,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
for invalid_payload in ( for invalid_payload in (
{"view": "overview", "sources": []}, {"view": "overview", "sources": []},
{"view": "accounts"}, {"view": "accounts"},
{"view": "accounts", "sources": [{"name": "code-plan"}]}, {"view": "accounts", "sources": [{"name": "codeplan"}]},
{ {
"view": "accounts", "view": "accounts",
"sources": [ "sources": [
@@ -618,7 +672,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
self.assertEqual(mod.monitor_availability(rows[0], status_payload), "ok") self.assertEqual(mod.monitor_availability(rows[0], status_payload), "ok")
self.assertIn("availability", out.getvalue()) self.assertIn("availability", out.getvalue())
self.assertNotIn("5h bal", out.getvalue()) self.assertNotIn("5h bal", out.getvalue())
self.assertIn(" ok\n", out.getvalue()) self.assertIn(" ok ", out.getvalue())
def test_monitor_availability_is_dash_without_bound_monitor(self) -> None: def test_monitor_availability_is_dash_without_bound_monitor(self) -> None:
mod = load_module() mod = load_module()
@@ -823,7 +877,7 @@ class WorkspaceTests(unittest.TestCase):
self.assertEqual(accounts["accounts"][0]["id"], "9007199254740993") self.assertEqual(accounts["accounts"][0]["id"], "9007199254740993")
self.assertEqual(status["channel_monitors"]["items"][0]["id"], "9007199254740995") self.assertEqual(status["channel_monitors"]["items"][0]["id"], "9007199254740995")
self.assertEqual(pricing["sources"][0]["name"], "code-plan") self.assertEqual(pricing["sources"][0]["name"], "codeplan")
self.assertEqual(len(logs["data"]["items"]), 1) self.assertEqual(len(logs["data"]["items"]), 1)
self.assertEqual(logs["data"]["items"][0]["id"], "9007199254740997") self.assertEqual(logs["data"]["items"][0]["id"], "9007199254740997")
self.assertEqual(logs["data"]["items"][0]["instance"], "server6") self.assertEqual(logs["data"]["items"][0]["instance"], "server6")
@@ -927,6 +981,12 @@ class WorkspaceTests(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "account contract"): with self.assertRaisesRegex(RuntimeError, "account contract"):
mod.fetch_workspace_payload("https://workspace.example.test/data", 3) mod.fetch_workspace_payload("https://workspace.example.test/data", 3)
invalid_pricing_timestamp = workspace_payload_fixture()
invalid_pricing_timestamp["accounts"]["accounts"][0]["pricing_last_fetch_at"] = "not-a-timestamp"
with mock.patch.object(mod, "fetch_workspace_response", return_value=(invalid_pricing_timestamp, "")):
with self.assertRaisesRegex(RuntimeError, "account contract"):
mod.fetch_workspace_payload("https://workspace.example.test/data", 3)
invalid_period = workspace_payload_fixture() invalid_period = workspace_payload_fixture()
invalid_period["traffic"].pop("ends_at") invalid_period["traffic"].pop("ends_at")
with mock.patch.object(mod, "fetch_workspace_response", return_value=(invalid_period, "")): with mock.patch.object(mod, "fetch_workspace_response", return_value=(invalid_period, "")):
@@ -1341,7 +1401,7 @@ class WorkspaceTests(unittest.TestCase):
"https://workspace.example.test/api/ui-data?view=workspace", 10 "https://workspace.example.test/api/ui-data?view=workspace", 10
) )
token_read.assert_not_called() token_read.assert_not_called()
self.assertIn("oai-quota-code-plan", out.getvalue()) self.assertIn("oai-quota-codeplan-primary", out.getvalue())
self.assertIn("wmy", out.getvalue()) self.assertIn("wmy", out.getvalue())
def test_empty_workspace_url_falls_back_to_legacy_direct_mode(self) -> None: def test_empty_workspace_url_falls_back_to_legacy_direct_mode(self) -> None:
@@ -1732,7 +1792,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
"accounts": [ "accounts": [
{ {
"id": 1, "id": 1,
"name": "oai-quota-code-plan", "name": "oai-quota-codeplan-primary",
"routing_group": "fast", "routing_group": "fast",
"provider": "openai", "provider": "openai",
"kind": "pay_as_you_go", "kind": "pay_as_you_go",
@@ -1745,7 +1805,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
"generated_at": "2026-07-24T12:00:00+08:00", "generated_at": "2026-07-24T12:00:00+08:00",
"sources": [ "sources": [
{ {
"name": "code-plan", "name": "codeplan",
"source_kind": "newapi", "source_kind": "newapi",
"health_state": "healthy", "health_state": "healthy",
"last_success_at": "2026-07-24T12:00:00+08:00", "last_success_at": "2026-07-24T12:00:00+08:00",
@@ -1839,7 +1899,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
self.assertGreater(logs_table.max_scroll_x, 0) self.assertGreater(logs_table.max_scroll_x, 0)
self.assertGreater(errors_table.max_scroll_x, 0) self.assertGreater(errors_table.max_scroll_x, 0)
self.assertEqual(screen.log_rows[0]["cache_tokens"], 4150) self.assertEqual(screen.log_rows[0]["cache_tokens"], 4150)
self.assertEqual(screen.account_rows[0]["pricing_source"], "code-plan") self.assertEqual(screen.account_rows[0]["pricing_source"], "codeplan")
self.assertEqual(screen.account_rows[0]["pricing_cny"], 10) self.assertEqual(screen.account_rows[0]["pricing_cny"], 10)
self.assertGreater(screen.log_rows[0]["tokens_per_second"], 0) self.assertGreater(screen.log_rows[0]["tokens_per_second"], 0)
self.assertEqual(len({table.styles.background for table in tables}), 4) self.assertEqual(len({table.styles.background for table in tables}), 4)
@@ -1852,7 +1912,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
await pilot.pause() await pilot.pause()
self.assertEqual(type(app.screen).__name__, "PricingScreen") self.assertEqual(type(app.screen).__name__, "PricingScreen")
self.assertEqual(app.screen.focused.id, "pricing") self.assertEqual(app.screen.focused.id, "pricing")
self.assertEqual(app.screen.rows[0]["name"], "code-plan") self.assertEqual(app.screen.rows[0]["name"], "codeplan")
async def test_workspace_dashboard_handles_initial_endpoint_failure(self) -> None: async def test_workspace_dashboard_handles_initial_endpoint_failure(self) -> None:
@@ -1936,7 +1996,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
screen = app.screen screen = app.screen
self.assertEqual(type(screen).__name__, "DashboardScreen") self.assertEqual(type(screen).__name__, "DashboardScreen")
self.assertEqual(app.sub_title, "Dashboard") self.assertEqual(app.sub_title, "Dashboard")
self.assertEqual(screen.account_rows[0]["name"], "oai-quota-code-plan") self.assertEqual(screen.account_rows[0]["name"], "oai-quota-codeplan-primary")
self.assertEqual(screen.error_rows[0]["key"], "wmy") self.assertEqual(screen.error_rows[0]["key"], "wmy")
self.assertNotIn("count", screen.error_rows[0]) self.assertNotIn("count", screen.error_rows[0])
self.assertEqual(len(fetch.call_args_list), 1) self.assertEqual(len(fetch.call_args_list), 1)
@@ -1960,7 +2020,7 @@ class PageSelectionTests(unittest.TestCase):
"generated_at": "2026-08-02T12:00:00Z", "generated_at": "2026-08-02T12:00:00Z",
"sources": [ "sources": [
{ {
"name": "code-plan", "name": "codeplan",
"source_kind": "newapi", "source_kind": "newapi",
"health_state": "healthy", "health_state": "healthy",
"last_success_at": "2026-08-02T11:59:00Z", "last_success_at": "2026-08-02T11:59:00Z",
@@ -1976,7 +2036,7 @@ class PageSelectionTests(unittest.TestCase):
mod.fetch_pricing_payload = original_fetch mod.fetch_pricing_payload = original_fetch
self.assertEqual(rc, 0) self.assertEqual(rc, 0)
self.assertIn("code-plan", out.getvalue()) self.assertIn("codeplan", out.getvalue())
self.assertIn("¥10", out.getvalue()) self.assertIn("¥10", out.getvalue())
def test_once_pricing_failure_is_redacted(self) -> None: def test_once_pricing_failure_is_redacted(self) -> None:
@@ -2004,14 +2064,14 @@ class PageSelectionTests(unittest.TestCase):
"source_name": "test", "source_name": "test",
"totals": {"total_accounts": 2, "usable_accounts": 2, "today_cost_usd": 0}, "totals": {"total_accounts": 2, "usable_accounts": 2, "today_cost_usd": 0},
"accounts": [ "accounts": [
{"id": 1, "name": "oai-quota-code-plan", "routing_group": "fast", "kind": "quota_limited"}, {"id": 1, "name": "oai-quota-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"},
{"id": 2, "name": "oai-sub-code-plan", "routing_group": "fast", "kind": "quota_limited"}, {"id": 2, "name": "oai-sub-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"},
], ],
} }
pricing_payload = { pricing_payload = {
"sources": [ "sources": [
{ {
"name": "code-plan", "name": "codeplan",
"source_kind": "newapi", "source_kind": "newapi",
"health_state": "healthy", "health_state": "healthy",
"last_success_at": "2026-08-02T12:00:00Z", "last_success_at": "2026-08-02T12:00:00Z",
@@ -2044,9 +2104,9 @@ class PageSelectionTests(unittest.TestCase):
text = out.getvalue() text = out.getvalue()
self.assertEqual(rc, 0) self.assertEqual(rc, 0)
self.assertIn("src cny", text) self.assertIn("src cny", text)
self.assertIn("oai-quota-code-plan", text) self.assertIn("oai-quota-codeplan-primary", text)
self.assertIn("oai-sub-code-plan", text) self.assertIn("oai-sub-codeplan-primary", text)
self.assertGreaterEqual(text.count("code-plan"), 2) self.assertGreaterEqual(text.count("codeplan"), 2)
self.assertIn("¥10", text) self.assertIn("¥10", text)
def test_dedicated_page_flags_are_mutually_exclusive(self) -> None: def test_dedicated_page_flags_are_mutually_exclusive(self) -> None:
Generated
+1 -1
View File
@@ -85,7 +85,7 @@ wheels = [
[[package]] [[package]]
name = "shusub2" name = "shusub2"
version = "0.3.6" version = "0.3.7"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "textual" }, { name = "textual" },