feat: project current account rate multipliers
This commit is contained in:
@@ -69,10 +69,10 @@ refresh。组件失败时服务端保留对应 last-good 数据,并在 `compon
|
||||
|
||||
Workspace 当前提供以下有界 projection:
|
||||
|
||||
- `accounts`:账号摘要、quota window、当日用量和 provider/group/status 字段。
|
||||
- `accounts`:账号摘要、quota window、当日用量和 provider/group/status 字段;如能按当前 server6 upstream key-group binding 关联,还包含 `account_rate_multiplier` 原始有效倍率和 `account_rate_multiplier_cny` 展示倍率。
|
||||
- `status`:channel monitor 摘要。
|
||||
- `sources`:Pricing Monitor 已有的脱敏 source 余额与健康状态。
|
||||
- `traffic.requests`:仅 server6 的最新有界请求样本。
|
||||
- `traffic.requests`:仅 server6 的最新有界请求样本;每行带对应 account 的当前 `account_rate_multiplier` / `account_rate_multiplier_cny`,无法稳定关联时为 `null`。
|
||||
- `traffic.errors`:server6/server4 的有界逐条错误事件,保留节点、时间、key/account、结构化错误类别和状态链路,不做错误分组或 per-row count。
|
||||
- `traffic.keys`:仅 server6 按 `Asia/Shanghai` 自然日完整聚合的 key 使用量;事件样本上限不会截断其 request/token/cost totals,payload 同时携带统计日期和时区。
|
||||
|
||||
@@ -120,8 +120,8 @@ Accounts 只在 canonical 名称 `{family}-quota-{source}` 或
|
||||
provider、URL、display name 或模糊文本推断;source 为 error/stale 时保留名称和状态,
|
||||
但不伪造 CNY 数值。
|
||||
|
||||
Requests 展示 server6 的最新有界明细:key、account、model、token bucket、actual cost、first-token latency、
|
||||
duration 和 decode throughput。Errors 展示 server6/server4 的逐条 instance、事件时间、status path、key、
|
||||
Requests 展示 server6 的最新有界明细:key、account、当前 account multiplier、model、token bucket、actual cost、first-token latency、
|
||||
duration 和 decode throughput。该 multiplier 来自当前 account binding,不代表请求发生时的历史计费倍率。Errors 展示 server6/server4 的逐条 instance、事件时间、status path、key、
|
||||
account、model、phase/type/owner,不显示聚合次数。Key usage 和对应 Traffic Account usage 是 server6
|
||||
完整当天 rollup;Errors 与 Requests 仍是事件样本。所有 workspace Traffic 都使用服务器声明的 `Asia/Shanghai` 自然日,不依赖客户端本地日期或滚动
|
||||
24 小时窗口;每个 Traffic projection 显式携带日期、时区和 `[00:00, 次日 00:00)` 边界。
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "shusub2"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
description = "Aggregated operations TUI for Sub2API"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
+34
-6
@@ -27,7 +27,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
|
||||
APP_NAME = "shusub2"
|
||||
FALLBACK_VERSION = "0.3.4"
|
||||
FALLBACK_VERSION = "0.3.5"
|
||||
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"
|
||||
@@ -392,6 +392,18 @@ def format_cny(value: Any) -> str:
|
||||
return "-" if amount == "-" else f"¥{amount}"
|
||||
|
||||
|
||||
def format_multiplier(value: Any) -> str:
|
||||
amount = optional_number(value)
|
||||
if amount is None:
|
||||
return "-"
|
||||
return f"{amount:.8g}x"
|
||||
|
||||
|
||||
def account_rate_value(item: dict[str, Any]) -> float | None:
|
||||
display = optional_number(item.get("account_rate_multiplier_cny"))
|
||||
return display if display is not None else optional_number(item.get("account_rate_multiplier"))
|
||||
|
||||
|
||||
def format_count(value: Any) -> str:
|
||||
number = as_int(value)
|
||||
if abs(number) >= 1_000_000:
|
||||
@@ -1172,6 +1184,9 @@ def normalize_log_rows(payload: dict[str, Any], filter_text: str = "") -> list[d
|
||||
"node": node,
|
||||
"key": key_name,
|
||||
"account": account_name,
|
||||
"account_rate_multiplier": optional_number(item.get("account_rate_multiplier")),
|
||||
"account_rate_multiplier_cny": optional_number(item.get("account_rate_multiplier_cny")),
|
||||
"account_multiplier": account_rate_value(item),
|
||||
"user": user_name,
|
||||
"model": model,
|
||||
"upstream_model": upstream_model,
|
||||
@@ -1225,7 +1240,7 @@ def log_detail_line(row: dict[str, Any]) -> str:
|
||||
if row["effort"] != "-":
|
||||
model = f"{model} ({row['effort']})"
|
||||
detail = (
|
||||
f"{row['time']} | {row['node']} | key {row['key']} | account {row['account']} | user {row['user']} | {model} | {row['type']} | "
|
||||
f"{row['time']} | {row['node']} | key {row['key']} | account {row['account']} | multiplier {format_multiplier(row['account_multiplier'])} | user {row['user']} | {model} | {row['type']} | "
|
||||
f"tokens in {format_count(row['input_tokens'])} out {format_count(row['output_tokens'])} "
|
||||
f"cache_w {format_count(row['cache_creation_tokens'])} cache_r {format_count(row['cache_read_tokens'])} | "
|
||||
f"cost {format_cost(row['cost'])} (actual {format_cost(row['actual_cost'])}) | "
|
||||
@@ -1240,12 +1255,13 @@ def log_detail_line(row: dict[str, Any]) -> str:
|
||||
def print_logs_once(payload: dict[str, Any], filter_text: str = "") -> None:
|
||||
rows = normalize_log_rows(payload, filter_text)
|
||||
print(logs_summary_line(payload, len(rows)))
|
||||
print("node key account model effort type input output cache tokens cost first duration tok/s time age")
|
||||
print("node key account multiplier model effort type input output cache tokens cost first duration tok/s time age")
|
||||
for row in rows:
|
||||
print(
|
||||
f"{row['node'][:8]:<9} "
|
||||
f"{row['key'][:20]:<21} "
|
||||
f"{row['account'][:20]:<21} "
|
||||
f"{format_multiplier(row['account_multiplier']):<11} "
|
||||
f"{row['model'][:24]:<25} "
|
||||
f"{row['effort']:<7} "
|
||||
f"{row['type']:<7} "
|
||||
@@ -1736,6 +1752,9 @@ def normalize_account_rows(
|
||||
"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 "-",
|
||||
"account_rate_multiplier": optional_number(account.get("account_rate_multiplier")),
|
||||
"account_rate_multiplier_cny": optional_number(account.get("account_rate_multiplier_cny")),
|
||||
"account_multiplier": account_rate_value(account),
|
||||
"provider": provider_label(account),
|
||||
"kind": str(account.get("kind") or "unknown"),
|
||||
"kind_label": kind_label(account.get("kind")),
|
||||
@@ -1922,13 +1941,14 @@ def print_once(
|
||||
print(f"sources: {pricing_error}")
|
||||
elif pricing_payload:
|
||||
print(pricing_summary(pricing_payload))
|
||||
print("name provider group source src cny src state daily today tokens req kind 5h 7d reset status availability")
|
||||
print("name provider group source multiplier src cny src state daily today tokens req kind 5h 7d reset status availability")
|
||||
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['daily_quota_cell']:<10} "
|
||||
@@ -2078,7 +2098,7 @@ def run_textual(
|
||||
self.app.sub_title = "Dashboard"
|
||||
self.configure_table(
|
||||
self.query_one("#accounts", DataTable),
|
||||
("ACCOUNT", "Group", "Source", "Src CNY", "Src state", "Today", "Daily", "5h", "7d", "Avail"),
|
||||
("ACCOUNT", "Group", "Source", "Multiplier", "Src CNY", "Src state", "Today", "Daily", "5h", "7d", "Avail"),
|
||||
)
|
||||
self.configure_table(
|
||||
self.query_one("#keys", DataTable),
|
||||
@@ -2090,6 +2110,7 @@ def run_textual(
|
||||
"Node",
|
||||
"REQUEST KEY",
|
||||
"Account",
|
||||
"Multiplier",
|
||||
"Model",
|
||||
"First",
|
||||
"Duration",
|
||||
@@ -2241,6 +2262,7 @@ def run_textual(
|
||||
row["name"],
|
||||
row["routing_group"],
|
||||
row["pricing_source"],
|
||||
format_multiplier(row["account_multiplier"]),
|
||||
format_cny(row["pricing_cny"]),
|
||||
row["pricing_state"],
|
||||
format_cost(row["today_cost_usd"]),
|
||||
@@ -2285,6 +2307,7 @@ def run_textual(
|
||||
row["node"],
|
||||
Text(row["key"], style=color) if color else row["key"],
|
||||
row["account"],
|
||||
format_multiplier(row["account_multiplier"]),
|
||||
row["model"],
|
||||
format_seconds(row["first_token_ms"]),
|
||||
format_seconds(row["duration_ms"]),
|
||||
@@ -2418,6 +2441,7 @@ def run_textual(
|
||||
if table_id == "accounts":
|
||||
detail = (
|
||||
f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | "
|
||||
f"multiplier {format_multiplier(row['account_multiplier'])} | "
|
||||
f"daily {row['daily_quota_cell']} | "
|
||||
f"5h {row['five_hour']} | "
|
||||
f"7d {row['weekly']} | "
|
||||
@@ -2499,7 +2523,7 @@ def run_textual(
|
||||
table = self.query_one("#accounts", DataTable)
|
||||
table.cursor_type = "row"
|
||||
table.zebra_stripes = True
|
||||
table.add_columns("Name", "Provider", "Group", "Source", "Src CNY", "Src state", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status", "Availability")
|
||||
table.add_columns("Name", "Provider", "Group", "Source", "Multiplier", "Src CNY", "Src state", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status", "Availability")
|
||||
keys_table = self.query_one("#keys", DataTable)
|
||||
keys_table.cursor_type = "row"
|
||||
keys_table.zebra_stripes = True
|
||||
@@ -2597,6 +2621,7 @@ def run_textual(
|
||||
row["provider"],
|
||||
row["routing_group"],
|
||||
row["pricing_source"],
|
||||
format_multiplier(row["account_multiplier"]),
|
||||
format_cny(row["pricing_cny"]),
|
||||
row["pricing_state"],
|
||||
row["daily_quota_cell"],
|
||||
@@ -2627,6 +2652,7 @@ def run_textual(
|
||||
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"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) | "
|
||||
f"5h {row['five_hour']} | 7d {row['weekly']} | "
|
||||
@@ -2791,6 +2817,7 @@ def run_textual(
|
||||
"Node",
|
||||
"Key",
|
||||
"Account",
|
||||
"Multiplier",
|
||||
"Model",
|
||||
"Effort",
|
||||
"Type",
|
||||
@@ -2865,6 +2892,7 @@ def run_textual(
|
||||
row["node"],
|
||||
Text(row["key"], style=color) if color else row["key"],
|
||||
row["account"],
|
||||
format_multiplier(row["account_multiplier"]),
|
||||
row["model"],
|
||||
row["effort"],
|
||||
row["type"],
|
||||
|
||||
@@ -67,6 +67,8 @@ def workspace_payload_fixture() -> dict[str, object]:
|
||||
"provider": "openai",
|
||||
"kind": "quota_limited",
|
||||
"usable": True,
|
||||
"account_rate_multiplier": 0.06,
|
||||
"account_rate_multiplier_cny": 0.003,
|
||||
}
|
||||
],
|
||||
},
|
||||
@@ -105,6 +107,8 @@ def workspace_payload_fixture() -> dict[str, object]:
|
||||
"api_key_name": "wmy",
|
||||
"account_id": "9",
|
||||
"account_name": "oai-quota-code-plan",
|
||||
"account_rate_multiplier": 0.06,
|
||||
"account_rate_multiplier_cny": 0.003,
|
||||
"model": "gpt-5.5",
|
||||
"input_tokens": 100,
|
||||
"output_tokens": 50,
|
||||
@@ -803,7 +807,9 @@ class WorkspaceTests(unittest.TestCase):
|
||||
normalized_accounts = mod.normalize_account_rows(accounts, pricing_payload=pricing)
|
||||
normalized_logs = mod.normalize_log_rows(logs)
|
||||
self.assertEqual(normalized_accounts[0]["id"], 9007199254740993)
|
||||
self.assertEqual(normalized_accounts[0]["account_multiplier"], 0.003)
|
||||
self.assertEqual(normalized_logs[0]["id"], 9007199254740997)
|
||||
self.assertEqual(normalized_logs[0]["account_multiplier"], 0.003)
|
||||
self.assertEqual(normalized_logs[0]["cost"], 0.25)
|
||||
self.assertEqual(mod.as_int("9007199254740999"), 9007199254740999)
|
||||
self.assertEqual(len(keys["items"]), 1)
|
||||
|
||||
Reference in New Issue
Block a user