feat: show canonical account source and groups
This commit is contained in:
@@ -69,7 +69,7 @@ refresh。组件失败时服务端保留对应 last-good 数据,并在 `compon
|
||||
|
||||
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 摘要。
|
||||
- `sources`:Pricing Monitor 已有的脱敏 source 余额与健康状态。
|
||||
- `traffic.requests`:仅 server6 的最新有界请求样本;每行带对应 account 的当前 `account_rate_multiplier` / `account_rate_multiplier_cny`,无法稳定关联时为 `null`。
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "shusub2"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
description = "Aggregated operations TUI for Sub2API"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
+104
-27
@@ -27,7 +27,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
|
||||
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_CONFIG_FILE = "~/.config/shusub2/workspace-url"
|
||||
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_STOPWORDS = {"response", "responses", "monitor"}
|
||||
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,
|
||||
)
|
||||
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
|
||||
if str(row.get("last_test_state") or "") not in {"yes", "partial", "no", "NA"}:
|
||||
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(
|
||||
row.get("last_test_model"), str
|
||||
):
|
||||
@@ -1783,14 +1803,24 @@ def normalize_account_rows(
|
||||
for account in payload.get("accounts") or []:
|
||||
if not isinstance(account, dict):
|
||||
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(
|
||||
str(account.get(key) or "")
|
||||
for key in (
|
||||
"id",
|
||||
"name",
|
||||
"routing_group",
|
||||
"account_group_names",
|
||||
"provider",
|
||||
"pricing_source",
|
||||
"kind",
|
||||
"status",
|
||||
"account_type",
|
||||
@@ -1809,10 +1839,27 @@ def normalize_account_rows(
|
||||
"id": as_int(account.get("id")),
|
||||
"name": str(account.get("name") or ""),
|
||||
"routing_group": routing_group,
|
||||
"groups": groups,
|
||||
"base_url_hash": str(account.get("base_url_hash") or ""),
|
||||
"pricing_source": str(pricing_source.get("name") or "-") if pricing_source else "-",
|
||||
"pricing_cny": pricing_source.get("balance_cny") if pricing_source and pricing_source.get("status") == "healthy" else None,
|
||||
"pricing_state": str(pricing_source.get("status") or "-") if pricing_source else "-",
|
||||
"pricing_source": (
|
||||
explicit_source_name
|
||||
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_unit": str(pricing_source.get("unit") 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:
|
||||
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]:
|
||||
@@ -1930,6 +1977,32 @@ def pricing_source_for_account(
|
||||
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:
|
||||
source_rows = rows if rows is not None else normalize_pricing_rows(payload)
|
||||
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"))
|
||||
raw_balance = format_amount(row.get("pricing_balance"))
|
||||
unit = str(row.get("pricing_unit") or "-")
|
||||
state = str(row.get("pricing_state") or "-")
|
||||
updated = str(row.get("pricing_updated") or "-")
|
||||
return f"source {source} | CNY {cny} | raw {raw_balance} {unit} | {state} | updated {updated}"
|
||||
return (
|
||||
f"source {source} | CNY {cny} | raw {raw_balance} {unit} | "
|
||||
f"last fetch {row.get('pricing_fetch') or '-'}"
|
||||
)
|
||||
|
||||
|
||||
def print_pricing_once(payload: dict[str, Any], filter_text: str = "") -> None:
|
||||
@@ -2011,16 +2085,15 @@ def print_once(
|
||||
print(f"sources: {pricing_error}")
|
||||
elif 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):
|
||||
print(
|
||||
f"{row['name'][:30]:<30} "
|
||||
f"{row['provider']:<9} "
|
||||
f"{row['routing_group']:<6} "
|
||||
f"{row['pricing_source']:<16} "
|
||||
f"{format_multiplier(row['account_multiplier']):<11} "
|
||||
f"{format_cny(row['pricing_cny']):<13} "
|
||||
f"{row['pricing_state']:<12} "
|
||||
f"{row['pricing_fetch']:<21} "
|
||||
f"{row['daily_quota_cell']:<10} "
|
||||
f"{format_cost(row['today_cost_usd']):<10} "
|
||||
f"{format_count(row['today_tokens']):<8} "
|
||||
@@ -2035,7 +2108,8 @@ def print_once(
|
||||
f"{row['last_test']:<10} "
|
||||
f"{row['last_test_time']:<13} "
|
||||
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;
|
||||
# narrow terminals use DataTable's horizontal scrolling instead.
|
||||
for label in columns:
|
||||
table.add_column(label)
|
||||
table.add_column(label, width=32 if label == "Groups" else None)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.app.sub_title = "Dashboard"
|
||||
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:
|
||||
account_columns += ("Active", "Schedulable", "Last test", "Last test time", "Last test model")
|
||||
account_columns += ("Avail",)
|
||||
account_columns += ("Avail", "Groups")
|
||||
self.configure_table(
|
||||
self.query_one("#accounts", DataTable),
|
||||
account_columns,
|
||||
@@ -2341,11 +2415,10 @@ def run_textual(
|
||||
self.account_by_key[key] = row
|
||||
cells: list[Any] = [
|
||||
row["name"],
|
||||
row["routing_group"],
|
||||
row["pricing_source"],
|
||||
format_multiplier(row["account_multiplier"]),
|
||||
format_cny(row["pricing_cny"]),
|
||||
row["pricing_state"],
|
||||
row["pricing_fetch"],
|
||||
format_cost(row["today_cost_usd"]),
|
||||
row["daily_quota_cell"],
|
||||
row["five_hour"],
|
||||
@@ -2362,6 +2435,7 @@ def run_textual(
|
||||
)
|
||||
)
|
||||
cells.append(monitor_availability(row, self.status_payload))
|
||||
cells.append(row["groups"])
|
||||
table.add_row(*cells, key=key)
|
||||
|
||||
def render_keys(self) -> None:
|
||||
@@ -2531,7 +2605,7 @@ def run_textual(
|
||||
def render_detail(self, table_id: str, row: dict[str, Any]) -> None:
|
||||
if table_id == "accounts":
|
||||
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"daily {row['daily_quota_cell']} | "
|
||||
f"5h {row['five_hour']} | "
|
||||
@@ -2618,12 +2692,13 @@ def run_textual(
|
||||
table.cursor_type = "row"
|
||||
table.zebra_stripes = True
|
||||
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:
|
||||
account_columns.extend(("Active", "Schedulable", "Last test", "Last test time", "Last test model"))
|
||||
account_columns.append("Availability")
|
||||
table.add_columns(*account_columns)
|
||||
account_columns.extend(("Availability", "Groups"))
|
||||
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.cursor_type = "row"
|
||||
keys_table.zebra_stripes = True
|
||||
@@ -2719,11 +2794,10 @@ def run_textual(
|
||||
cells: list[Any] = [
|
||||
row["name"],
|
||||
row["provider"],
|
||||
row["routing_group"],
|
||||
row["pricing_source"],
|
||||
format_multiplier(row["account_multiplier"]),
|
||||
format_cny(row["pricing_cny"]),
|
||||
row["pricing_state"],
|
||||
row["pricing_fetch"],
|
||||
row["daily_quota_cell"],
|
||||
format_cost(row["today_cost_usd"]),
|
||||
format_count(row["today_tokens"]),
|
||||
@@ -2744,7 +2818,10 @@ def run_textual(
|
||||
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)
|
||||
self.query_one("#summary", Static).update(summary_line(self.payload))
|
||||
if self.rows:
|
||||
@@ -2761,7 +2838,7 @@ def run_textual(
|
||||
def render_detail(self, row: dict[str, Any]) -> None:
|
||||
raw = row.get("raw") if isinstance(row.get("raw"), dict) else {}
|
||||
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"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) | "
|
||||
|
||||
+103
-43
@@ -43,7 +43,7 @@ def workspace_payload_fixture() -> dict[str, object]:
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "code-plan",
|
||||
"name": "codeplan",
|
||||
"source_kind": "newapi",
|
||||
"health_state": "healthy",
|
||||
"balance_available": True,
|
||||
@@ -62,7 +62,7 @@ def workspace_payload_fixture() -> dict[str, object]:
|
||||
"accounts": [
|
||||
{
|
||||
"id": "9007199254740993",
|
||||
"name": "oai-quota-code-plan",
|
||||
"name": "oai-quota-codeplan-primary",
|
||||
"routing_group": "fast",
|
||||
"provider": "openai",
|
||||
"kind": "quota_limited",
|
||||
@@ -98,7 +98,7 @@ def workspace_payload_fixture() -> dict[str, object]:
|
||||
"items": [
|
||||
{
|
||||
"id": "9007199254740995",
|
||||
"name": "oai-quota-code-plan",
|
||||
"name": "oai-quota-codeplan-primary",
|
||||
"provider": "openai",
|
||||
"latest_status": "success",
|
||||
}
|
||||
@@ -127,7 +127,7 @@ def workspace_payload_fixture() -> dict[str, object]:
|
||||
"api_key_id": "7",
|
||||
"api_key_name": "wmy",
|
||||
"account_id": "9",
|
||||
"account_name": "oai-quota-code-plan",
|
||||
"account_name": "oai-quota-codeplan-primary",
|
||||
"account_rate_multiplier": 0.06,
|
||||
"account_rate_multiplier_cny": 0.003,
|
||||
"model": "gpt-5.5",
|
||||
@@ -149,7 +149,7 @@ def workspace_payload_fixture() -> dict[str, object]:
|
||||
"api_key_id": "7",
|
||||
"api_key_name": "wmy",
|
||||
"account_id": "9",
|
||||
"account_name": "oai-quota-code-plan",
|
||||
"account_name": "oai-quota-codeplan-primary",
|
||||
"model": "gpt-5.5",
|
||||
"error_type": "api_error",
|
||||
"error_source": "upstream_http",
|
||||
@@ -245,19 +245,19 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
|
||||
mod = load_module()
|
||||
accounts_payload = {
|
||||
"accounts": [
|
||||
{"id": 1, "name": "oai-quota-code-plan", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 2, "name": "oai-quotaonly-code-plan", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 3, "name": "oai-sub-code-plan", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 1, "name": "oai-quota-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 2, "name": "ordinary-account", "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": 5, "name": "oai-quota-ycy", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 6, "name": "oai-quota-code--plan", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 7, "name": "oai-quota-code-plan-", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 5, "name": "oai-quota-ycy-primary", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 6, "name": "oai-quota-codeplan--primary", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 7, "name": "oai-quota-codeplan-primary-", "routing_group": "fast", "kind": "quota_limited"},
|
||||
]
|
||||
}
|
||||
pricing_payload = {
|
||||
"sources": [
|
||||
{
|
||||
"name": "code-plan",
|
||||
"name": "codeplan",
|
||||
"source_kind": "newapi",
|
||||
"health_state": "healthy",
|
||||
"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)}
|
||||
|
||||
self.assertEqual(rows["oai-quota-code-plan"]["pricing_source"], "code-plan")
|
||||
self.assertEqual(rows["oai-quota-code-plan"]["pricing_cny"], 10)
|
||||
self.assertEqual(rows["oai-quotaonly-code-plan"]["pricing_source"], "code-plan")
|
||||
self.assertEqual(rows["oai-sub-code-plan"]["pricing_source"], "-")
|
||||
self.assertEqual(rows["oai-quota-codeplan-primary"]["pricing_source"], "codeplan")
|
||||
self.assertEqual(rows["oai-quota-codeplan-primary"]["pricing_cny"], 10)
|
||||
self.assertEqual(rows["ordinary-account"]["pricing_source"], "-")
|
||||
self.assertEqual(rows["oai-sub-codeplan-primary"]["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"]["pricing_state"], "error")
|
||||
self.assertIsNone(rows["oai-quota-ycy"]["pricing_cny"])
|
||||
self.assertEqual(rows["oai-quota-code--plan"]["pricing_source"], "-")
|
||||
self.assertEqual(rows["oai-quota-code-plan-"]["pricing_source"], "-")
|
||||
self.assertEqual(rows["oai-quota-ycy-primary"]["pricing_source"], "ycy")
|
||||
self.assertEqual(rows["oai-quota-ycy-primary"]["pricing_state"], "error")
|
||||
self.assertIsNone(rows["oai-quota-ycy-primary"]["pricing_cny"])
|
||||
self.assertEqual(rows["oai-quota-codeplan--primary"]["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_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:
|
||||
mod = load_module()
|
||||
@@ -296,7 +348,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
|
||||
"generated_at": "2026-08-02T12:00:00Z",
|
||||
"sources": [
|
||||
{
|
||||
"name": "code-plan",
|
||||
"name": "codeplan",
|
||||
"source_kind": "newapi",
|
||||
"health_state": "healthy",
|
||||
"last_success_at": "2026-08-02T11:59:00Z",
|
||||
@@ -316,11 +368,13 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
|
||||
|
||||
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_cny"], 10)
|
||||
self.assertEqual(rows[0]["unit"], "quota")
|
||||
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.assertIsNone(rows[1]["balance"])
|
||||
self.assertIsNone(rows[1]["balance_cny"])
|
||||
@@ -345,7 +399,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out):
|
||||
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("¥10", out.getvalue())
|
||||
|
||||
@@ -386,7 +440,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
|
||||
"view": "accounts",
|
||||
"sources": [
|
||||
{
|
||||
"name": "code-plan",
|
||||
"name": "codeplan",
|
||||
"source_kind": "newapi",
|
||||
"health_state": "healthy",
|
||||
"balance_available": True,
|
||||
@@ -403,7 +457,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
|
||||
for invalid_payload in (
|
||||
{"view": "overview", "sources": []},
|
||||
{"view": "accounts"},
|
||||
{"view": "accounts", "sources": [{"name": "code-plan"}]},
|
||||
{"view": "accounts", "sources": [{"name": "codeplan"}]},
|
||||
{
|
||||
"view": "accounts",
|
||||
"sources": [
|
||||
@@ -618,7 +672,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
|
||||
self.assertEqual(mod.monitor_availability(rows[0], status_payload), "ok")
|
||||
self.assertIn("availability", 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:
|
||||
mod = load_module()
|
||||
@@ -823,7 +877,7 @@ class WorkspaceTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(accounts["accounts"][0]["id"], "9007199254740993")
|
||||
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(logs["data"]["items"][0]["id"], "9007199254740997")
|
||||
self.assertEqual(logs["data"]["items"][0]["instance"], "server6")
|
||||
@@ -927,6 +981,12 @@ class WorkspaceTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(RuntimeError, "account contract"):
|
||||
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["traffic"].pop("ends_at")
|
||||
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
|
||||
)
|
||||
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())
|
||||
|
||||
def test_empty_workspace_url_falls_back_to_legacy_direct_mode(self) -> None:
|
||||
@@ -1732,7 +1792,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
||||
"accounts": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "oai-quota-code-plan",
|
||||
"name": "oai-quota-codeplan-primary",
|
||||
"routing_group": "fast",
|
||||
"provider": "openai",
|
||||
"kind": "pay_as_you_go",
|
||||
@@ -1745,7 +1805,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
||||
"generated_at": "2026-07-24T12:00:00+08:00",
|
||||
"sources": [
|
||||
{
|
||||
"name": "code-plan",
|
||||
"name": "codeplan",
|
||||
"source_kind": "newapi",
|
||||
"health_state": "healthy",
|
||||
"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(errors_table.max_scroll_x, 0)
|
||||
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.assertGreater(screen.log_rows[0]["tokens_per_second"], 0)
|
||||
self.assertEqual(len({table.styles.background for table in tables}), 4)
|
||||
@@ -1852,7 +1912,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
||||
await pilot.pause()
|
||||
self.assertEqual(type(app.screen).__name__, "PricingScreen")
|
||||
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:
|
||||
@@ -1936,7 +1996,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
|
||||
screen = app.screen
|
||||
self.assertEqual(type(screen).__name__, "DashboardScreen")
|
||||
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.assertNotIn("count", screen.error_rows[0])
|
||||
self.assertEqual(len(fetch.call_args_list), 1)
|
||||
@@ -1960,7 +2020,7 @@ class PageSelectionTests(unittest.TestCase):
|
||||
"generated_at": "2026-08-02T12:00:00Z",
|
||||
"sources": [
|
||||
{
|
||||
"name": "code-plan",
|
||||
"name": "codeplan",
|
||||
"source_kind": "newapi",
|
||||
"health_state": "healthy",
|
||||
"last_success_at": "2026-08-02T11:59:00Z",
|
||||
@@ -1976,7 +2036,7 @@ class PageSelectionTests(unittest.TestCase):
|
||||
mod.fetch_pricing_payload = original_fetch
|
||||
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("code-plan", out.getvalue())
|
||||
self.assertIn("codeplan", out.getvalue())
|
||||
self.assertIn("¥10", out.getvalue())
|
||||
|
||||
def test_once_pricing_failure_is_redacted(self) -> None:
|
||||
@@ -2004,14 +2064,14 @@ class PageSelectionTests(unittest.TestCase):
|
||||
"source_name": "test",
|
||||
"totals": {"total_accounts": 2, "usable_accounts": 2, "today_cost_usd": 0},
|
||||
"accounts": [
|
||||
{"id": 1, "name": "oai-quota-code-plan", "routing_group": "fast", "kind": "quota_limited"},
|
||||
{"id": 2, "name": "oai-sub-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-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"},
|
||||
],
|
||||
}
|
||||
pricing_payload = {
|
||||
"sources": [
|
||||
{
|
||||
"name": "code-plan",
|
||||
"name": "codeplan",
|
||||
"source_kind": "newapi",
|
||||
"health_state": "healthy",
|
||||
"last_success_at": "2026-08-02T12:00:00Z",
|
||||
@@ -2044,9 +2104,9 @@ class PageSelectionTests(unittest.TestCase):
|
||||
text = out.getvalue()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("src cny", text)
|
||||
self.assertIn("oai-quota-code-plan", text)
|
||||
self.assertIn("oai-sub-code-plan", text)
|
||||
self.assertGreaterEqual(text.count("code-plan"), 2)
|
||||
self.assertIn("oai-quota-codeplan-primary", text)
|
||||
self.assertIn("oai-sub-codeplan-primary", text)
|
||||
self.assertGreaterEqual(text.count("codeplan"), 2)
|
||||
self.assertIn("¥10", text)
|
||||
|
||||
def test_dedicated_page_flags_are_mutually_exclusive(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user