feat: show dual-node account test status
This commit is contained in:
@@ -69,7 +69,7 @@ refresh。组件失败时服务端保留对应 last-good 数据,并在 `compon
|
|||||||
|
|
||||||
Workspace 当前提供以下有界 projection:
|
Workspace 当前提供以下有界 projection:
|
||||||
|
|
||||||
- `accounts`:账号摘要、quota window、当日用量和 provider/group/status 字段;如能按当前 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` 展示倍率。
|
||||||
- `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`。
|
||||||
@@ -131,7 +131,7 @@ account、model、phase/type/owner,不显示聚合次数。Key usage 和对应
|
|||||||
`--legacy-direct` 仅用于迁移对账和故障诊断。它恢复旧拓扑:
|
`--legacy-direct` 仅用于迁移对账和故障诊断。它恢复旧拓扑:
|
||||||
|
|
||||||
- Accounts helper `/api/tui/accounts`
|
- Accounts helper `/api/tui/accounts`
|
||||||
- `sub2api-status` `/api/status`
|
- `sub2api-status` `/api/status`(历史接口,已归档下线;仅作为旧客户端参数的兼容说明)
|
||||||
- Pricing Monitor `view=accounts`
|
- Pricing Monitor `view=accounts`
|
||||||
- Sub2API admin usage/key/error API
|
- Sub2API admin usage/key/error API
|
||||||
|
|
||||||
@@ -202,12 +202,15 @@ uv lock --check
|
|||||||
## Maintenance Notes
|
## Maintenance Notes
|
||||||
|
|
||||||
Workspace adapter 只负责把 server projection 映射为现有渲染模型,不得在普通刷新中
|
Workspace adapter 只负责把 server projection 映射为现有渲染模型,不得在普通刷新中
|
||||||
重新引入 legacy HTTP 请求。新增 projection 字段时应同时验证:字段 allowlist、字符串 ID、
|
重新引入 legacy HTTP 请求。当前 `sub2api-status` 与旧 Accounts helper 已归档下线,Pricing Monitor
|
||||||
|
受管模型中的 Workspace 输入已成对清空。由于含用户未提交 Rule Control 改动的 companion 不在本次部署范围,
|
||||||
|
远端 collector 暂保留旧环境并会显示 stale / partial;下一次安全的 selected deployment 或重启后,默认
|
||||||
|
`shusub2` Workspace 入口将返回 `503 workspace data unavailable`。新增替代 projection 字段时应同时验证:字段 allowlist、字符串 ID、
|
||||||
finite number、payload 上限、单请求 cache、错误聚合次数和 legacy fallback。
|
finite number、payload 上限、单请求 cache、错误聚合次数和 legacy fallback。
|
||||||
|
|
||||||
`cliproxy-codex-quota` 与 `sub2api-status` 当前仍可作为 Aggregation Hub 的过渡采集输入和
|
`cliproxy-codex-quota` 可继续作为 Aggregation Hub 的过渡采集输入;`sub2api-status` 已归档下线,
|
||||||
双读对照,但不再是默认 TUI 客户端直连依赖。移除这些过渡输入前,必须先完成 server2
|
不得再作为采集输入或双读对照。恢复 Workspace 前,必须先完成新的服务端 projection、server2 聚合字段
|
||||||
聚合字段对账和 AstrBot `view=alerts` 连续性验证。
|
对账和 AstrBot `view=alerts` 连续性验证。
|
||||||
|
|
||||||
## Tests And Release
|
## Tests And Release
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "shusub2"
|
name = "shusub2"
|
||||||
version = "0.3.5"
|
version = "0.3.6"
|
||||||
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"
|
||||||
|
|||||||
+123
-10
@@ -27,7 +27,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|||||||
|
|
||||||
|
|
||||||
APP_NAME = "shusub2"
|
APP_NAME = "shusub2"
|
||||||
FALLBACK_VERSION = "0.3.5"
|
FALLBACK_VERSION = "0.3.6"
|
||||||
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"
|
||||||
@@ -664,6 +664,38 @@ def fetch_workspace_response(
|
|||||||
return payload, etag
|
return payload, etag
|
||||||
|
|
||||||
|
|
||||||
|
def workspace_account_contract_is_valid(rows: list[dict[str, Any]]) -> bool:
|
||||||
|
for row in rows:
|
||||||
|
if str(row.get("active_state") or "") not in {"yes", "partial", "no"}:
|
||||||
|
return False
|
||||||
|
if str(row.get("schedulable_state") or "") not in {"yes", "partial", "no"}:
|
||||||
|
return False
|
||||||
|
if str(row.get("last_test_state") or "") not in {"yes", "partial", "no", "NA"}:
|
||||||
|
return False
|
||||||
|
if not isinstance(row.get("last_test_time"), str) or not isinstance(
|
||||||
|
row.get("last_test_model"), str
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
node_status = row.get("node_status")
|
||||||
|
if not isinstance(node_status, dict):
|
||||||
|
return False
|
||||||
|
for node_name in ("server6", "server4"):
|
||||||
|
node = node_status.get(node_name)
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
return False
|
||||||
|
if node.get("active") is not None and not isinstance(node.get("active"), bool):
|
||||||
|
return False
|
||||||
|
if node.get("schedulable") is not None and not isinstance(node.get("schedulable"), bool):
|
||||||
|
return False
|
||||||
|
if str(node.get("last_test") or "") not in {"yes", "no", "NA"}:
|
||||||
|
return False
|
||||||
|
if not isinstance(node.get("last_test_time"), str) or not isinstance(
|
||||||
|
node.get("last_test_model"), str
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def fetch_workspace_payload(
|
def fetch_workspace_payload(
|
||||||
workspace_url: str,
|
workspace_url: str,
|
||||||
timeout: int,
|
timeout: int,
|
||||||
@@ -709,6 +741,8 @@ def fetch_workspace_payload(
|
|||||||
not isinstance(row, dict) for row in rows
|
not isinstance(row, dict) for row in rows
|
||||||
):
|
):
|
||||||
raise RuntimeError(f"Pricing Monitor returned invalid workspace {label}")
|
raise RuntimeError(f"Pricing Monitor returned invalid workspace {label}")
|
||||||
|
if not workspace_account_contract_is_valid(payload["accounts"]["accounts"]):
|
||||||
|
raise RuntimeError("Pricing Monitor returned invalid workspace account contract")
|
||||||
for label in ("requests", "keys"):
|
for label in ("requests", "keys"):
|
||||||
for row in traffic.get(label) or []:
|
for row in traffic.get(label) or []:
|
||||||
if str(row.get("instance") or row.get("node") or "").strip() != "server6":
|
if str(row.get("instance") or row.get("node") or "").strip() != "server6":
|
||||||
@@ -1708,6 +1742,36 @@ def reset_cell(account: dict[str, Any]) -> str:
|
|||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
|
|
||||||
|
def workspace_account_state(value: Any, *, last_test: bool = False) -> str:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return "yes" if value else "no"
|
||||||
|
allowed = {"yes", "partial", "no"}
|
||||||
|
if last_test:
|
||||||
|
allowed.add("NA")
|
||||||
|
state = str(value or "").strip()
|
||||||
|
return state if state in allowed else "-"
|
||||||
|
|
||||||
|
|
||||||
|
def workspace_account_node_detail(row: dict[str, Any]) -> str:
|
||||||
|
node_status = row.get("node_status")
|
||||||
|
if not isinstance(node_status, dict):
|
||||||
|
return "two-node -"
|
||||||
|
details: list[str] = []
|
||||||
|
for node_name in ("server6", "server4"):
|
||||||
|
node = node_status.get(node_name)
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
details.append(f"{node_name} unavailable")
|
||||||
|
continue
|
||||||
|
details.append(
|
||||||
|
f"{node_name} active {workspace_account_state(node.get('active'))} "
|
||||||
|
f"schedulable {workspace_account_state(node.get('schedulable'))} "
|
||||||
|
f"test {workspace_account_state(node.get('last_test'), last_test=True)} "
|
||||||
|
f"{short_time(node.get('last_test_time'))} "
|
||||||
|
f"{str(node.get('last_test_model') or '-')}"
|
||||||
|
)
|
||||||
|
return " | ".join(details) if details else "two-node -"
|
||||||
|
|
||||||
|
|
||||||
def normalize_account_rows(
|
def normalize_account_rows(
|
||||||
payload: dict[str, Any],
|
payload: dict[str, Any],
|
||||||
filter_text: str = "",
|
filter_text: str = "",
|
||||||
@@ -1762,6 +1826,12 @@ def normalize_account_rows(
|
|||||||
"account_type": str(account.get("account_type") or ""),
|
"account_type": str(account.get("account_type") or ""),
|
||||||
"plan": str(account.get("plan") or ""),
|
"plan": str(account.get("plan") or ""),
|
||||||
"priority": account.get("priority"),
|
"priority": account.get("priority"),
|
||||||
|
"active": workspace_account_state(account.get("active_state")),
|
||||||
|
"schedulable_state": workspace_account_state(account.get("schedulable_state")),
|
||||||
|
"last_test": workspace_account_state(account.get("last_test_state"), last_test=True),
|
||||||
|
"last_test_time": short_time(account.get("last_test_time")),
|
||||||
|
"last_test_model": str(account.get("last_test_model") or "-"),
|
||||||
|
"node_status": account.get("node_status") if isinstance(account.get("node_status"), dict) else {},
|
||||||
"today_cost_usd": as_float(account.get("today_cost_usd")),
|
"today_cost_usd": as_float(account.get("today_cost_usd")),
|
||||||
"today_actual_cost_usd": as_float(account.get("today_actual_cost_usd")),
|
"today_actual_cost_usd": as_float(account.get("today_actual_cost_usd")),
|
||||||
"today_tokens": as_int(account.get("today_tokens")),
|
"today_tokens": as_int(account.get("today_tokens")),
|
||||||
@@ -1941,7 +2011,7 @@ 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 availability")
|
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")
|
||||||
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} "
|
||||||
@@ -1960,6 +2030,11 @@ def print_once(
|
|||||||
f"{row['weekly']:<9} "
|
f"{row['weekly']:<9} "
|
||||||
f"{row['reset']:<11} "
|
f"{row['reset']:<11} "
|
||||||
f"{row['status']:<10} "
|
f"{row['status']:<10} "
|
||||||
|
f"{row['active']:<9} "
|
||||||
|
f"{row['schedulable_state']:<9} "
|
||||||
|
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 {})}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2096,9 +2171,15 @@ def run_textual(
|
|||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self.app.sub_title = "Dashboard"
|
self.app.sub_title = "Dashboard"
|
||||||
|
account_columns = (
|
||||||
|
"ACCOUNT", "Group", "Source", "Multiplier", "Src CNY", "Src state", "Today", "Daily", "5h", "7d"
|
||||||
|
)
|
||||||
|
if not legacy_direct:
|
||||||
|
account_columns += ("Active", "Schedulable", "Last test", "Last test time", "Last test model")
|
||||||
|
account_columns += ("Avail",)
|
||||||
self.configure_table(
|
self.configure_table(
|
||||||
self.query_one("#accounts", DataTable),
|
self.query_one("#accounts", DataTable),
|
||||||
("ACCOUNT", "Group", "Source", "Multiplier", "Src CNY", "Src state", "Today", "Daily", "5h", "7d", "Avail"),
|
account_columns,
|
||||||
)
|
)
|
||||||
self.configure_table(
|
self.configure_table(
|
||||||
self.query_one("#keys", DataTable),
|
self.query_one("#keys", DataTable),
|
||||||
@@ -2258,7 +2339,7 @@ def run_textual(
|
|||||||
for index, row in enumerate(self.account_rows):
|
for index, row in enumerate(self.account_rows):
|
||||||
key = f"account-{row['id']}-{index}"
|
key = f"account-{row['id']}-{index}"
|
||||||
self.account_by_key[key] = row
|
self.account_by_key[key] = row
|
||||||
table.add_row(
|
cells: list[Any] = [
|
||||||
row["name"],
|
row["name"],
|
||||||
row["routing_group"],
|
row["routing_group"],
|
||||||
row["pricing_source"],
|
row["pricing_source"],
|
||||||
@@ -2269,9 +2350,19 @@ def run_textual(
|
|||||||
row["daily_quota_cell"],
|
row["daily_quota_cell"],
|
||||||
row["five_hour"],
|
row["five_hour"],
|
||||||
row["weekly"],
|
row["weekly"],
|
||||||
monitor_availability(row, self.status_payload),
|
]
|
||||||
key=key,
|
if not legacy_direct:
|
||||||
|
cells.extend(
|
||||||
|
(
|
||||||
|
row["active"],
|
||||||
|
row["schedulable_state"],
|
||||||
|
row["last_test"],
|
||||||
|
row["last_test_time"],
|
||||||
|
row["last_test_model"],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
cells.append(monitor_availability(row, self.status_payload))
|
||||||
|
table.add_row(*cells, key=key)
|
||||||
|
|
||||||
def render_keys(self) -> None:
|
def render_keys(self) -> None:
|
||||||
filter_text = self.query_one("#filter", Input).value
|
filter_text = self.query_one("#filter", Input).value
|
||||||
@@ -2447,6 +2538,9 @@ def run_textual(
|
|||||||
f"7d {row['weekly']} | "
|
f"7d {row['weekly']} | "
|
||||||
f"today {format_cost(row['today_cost_usd'])}, "
|
f"today {format_cost(row['today_cost_usd'])}, "
|
||||||
f"{format_count(row['today_tokens'])} tokens, {format_count(row['today_requests'])} req | "
|
f"{format_count(row['today_tokens'])} tokens, {format_count(row['today_requests'])} req | "
|
||||||
|
f"active {row['active']} | schedulable {row['schedulable_state']} | "
|
||||||
|
f"last test {row['last_test']} {row['last_test_time']} {row['last_test_model']} | "
|
||||||
|
f"{workspace_account_node_detail(row)} | "
|
||||||
f"{account_pricing_detail(row)} | "
|
f"{account_pricing_detail(row)} | "
|
||||||
f"{monitor_detail(row, self.status_payload)}"
|
f"{monitor_detail(row, self.status_payload)}"
|
||||||
)
|
)
|
||||||
@@ -2523,7 +2617,13 @@ def run_textual(
|
|||||||
table = self.query_one("#accounts", DataTable)
|
table = self.query_one("#accounts", DataTable)
|
||||||
table.cursor_type = "row"
|
table.cursor_type = "row"
|
||||||
table.zebra_stripes = True
|
table.zebra_stripes = True
|
||||||
table.add_columns("Name", "Provider", "Group", "Source", "Multiplier", "Src CNY", "Src state", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status", "Availability")
|
account_columns = [
|
||||||
|
"Name", "Provider", "Group", "Source", "Multiplier", "Src CNY", "Src state", "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)
|
||||||
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
|
||||||
@@ -2616,7 +2716,7 @@ def run_textual(
|
|||||||
for row in self.rows:
|
for row in self.rows:
|
||||||
key = str(row["id"])
|
key = str(row["id"])
|
||||||
self.row_by_key[key] = row
|
self.row_by_key[key] = row
|
||||||
table.add_row(
|
cells: list[Any] = [
|
||||||
row["name"],
|
row["name"],
|
||||||
row["provider"],
|
row["provider"],
|
||||||
row["routing_group"],
|
row["routing_group"],
|
||||||
@@ -2633,9 +2733,19 @@ def run_textual(
|
|||||||
row["weekly"],
|
row["weekly"],
|
||||||
row["reset"],
|
row["reset"],
|
||||||
row["status"],
|
row["status"],
|
||||||
monitor_availability(row, self.status_payload),
|
]
|
||||||
key=key,
|
if not legacy_direct:
|
||||||
|
cells.extend(
|
||||||
|
(
|
||||||
|
row["active"],
|
||||||
|
row["schedulable_state"],
|
||||||
|
row["last_test"],
|
||||||
|
row["last_test_time"],
|
||||||
|
row["last_test_model"],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
cells.append(monitor_availability(row, self.status_payload))
|
||||||
|
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:
|
||||||
self.render_detail(self.rows[0])
|
self.render_detail(self.rows[0])
|
||||||
@@ -2660,6 +2770,9 @@ def run_textual(
|
|||||||
f"today {format_cost(row['today_cost_usd'])}, {format_count(row['today_tokens'])} tokens, "
|
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"{format_count(row['today_requests'])} req | latest {row['latest_usage_at']} | "
|
||||||
f"priority {row['priority'] if row['priority'] is not None else '-'} | "
|
f"priority {row['priority'] if row['priority'] is not None else '-'} | "
|
||||||
|
f"active {row['active']} | schedulable {row['schedulable_state']} | "
|
||||||
|
f"last test {row['last_test']} {row['last_test_time']} {row['last_test_model']} | "
|
||||||
|
f"{workspace_account_node_detail(row)} | "
|
||||||
f"{monitor_detail(row, self.status_payload)}"
|
f"{monitor_detail(row, self.status_payload)}"
|
||||||
)
|
)
|
||||||
error = str(raw.get("error") or "").strip()
|
error = str(raw.get("error") or "").strip()
|
||||||
|
|||||||
+71
-1
@@ -67,6 +67,27 @@ def workspace_payload_fixture() -> dict[str, object]:
|
|||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"kind": "quota_limited",
|
"kind": "quota_limited",
|
||||||
"usable": True,
|
"usable": True,
|
||||||
|
"active_state": "yes",
|
||||||
|
"schedulable_state": "partial",
|
||||||
|
"last_test_state": "partial",
|
||||||
|
"last_test_time": "2026-08-03T10:00:00Z",
|
||||||
|
"last_test_model": "gpt-5.6-luna",
|
||||||
|
"node_status": {
|
||||||
|
"server6": {
|
||||||
|
"active": True,
|
||||||
|
"schedulable": True,
|
||||||
|
"last_test": "yes",
|
||||||
|
"last_test_time": "2026-08-03T10:00:00Z",
|
||||||
|
"last_test_model": "gpt-5.6-luna",
|
||||||
|
},
|
||||||
|
"server4": {
|
||||||
|
"active": True,
|
||||||
|
"schedulable": False,
|
||||||
|
"last_test": "no",
|
||||||
|
"last_test_time": "2026-08-03T10:02:00Z",
|
||||||
|
"last_test_model": "gpt-5.6-mini",
|
||||||
|
},
|
||||||
|
},
|
||||||
"account_rate_multiplier": 0.06,
|
"account_rate_multiplier": 0.06,
|
||||||
"account_rate_multiplier_cny": 0.003,
|
"account_rate_multiplier_cny": 0.003,
|
||||||
}
|
}
|
||||||
@@ -730,6 +751,8 @@ def sample_logs_payload() -> dict:
|
|||||||
"api_key": {"id": 3, "name": "codex-main"},
|
"api_key": {"id": 3, "name": "codex-main"},
|
||||||
"account_id": 7,
|
"account_id": 7,
|
||||||
"account": {"id": 7, "name": "oai-sub-1"},
|
"account": {"id": 7, "name": "oai-sub-1"},
|
||||||
|
"account_rate_multiplier": 0.06,
|
||||||
|
"account_rate_multiplier_cny": 0.003,
|
||||||
"user": {"id": 1, "name": "shujakuin"},
|
"user": {"id": 1, "name": "shujakuin"},
|
||||||
"model": "gpt-5.5",
|
"model": "gpt-5.5",
|
||||||
"upstream_model": "gpt-5.5-codex",
|
"upstream_model": "gpt-5.5-codex",
|
||||||
@@ -807,10 +830,45 @@ class WorkspaceTests(unittest.TestCase):
|
|||||||
normalized_accounts = mod.normalize_account_rows(accounts, pricing_payload=pricing)
|
normalized_accounts = mod.normalize_account_rows(accounts, pricing_payload=pricing)
|
||||||
normalized_logs = mod.normalize_log_rows(logs)
|
normalized_logs = mod.normalize_log_rows(logs)
|
||||||
self.assertEqual(normalized_accounts[0]["id"], 9007199254740993)
|
self.assertEqual(normalized_accounts[0]["id"], 9007199254740993)
|
||||||
self.assertEqual(normalized_accounts[0]["account_multiplier"], 0.003)
|
self.assertEqual(normalized_accounts[0]["active"], "yes")
|
||||||
|
self.assertEqual(normalized_accounts[0]["schedulable_state"], "partial")
|
||||||
|
self.assertEqual(normalized_accounts[0]["last_test"], "partial")
|
||||||
|
self.assertEqual(normalized_accounts[0]["last_test_model"], "gpt-5.6-luna")
|
||||||
|
self.assertIn("server6 active yes", mod.workspace_account_node_detail(normalized_accounts[0]))
|
||||||
|
once_output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(once_output):
|
||||||
|
mod.print_once(accounts, pricing_payload=pricing)
|
||||||
|
self.assertIn("last-test", once_output.getvalue())
|
||||||
|
self.assertIn("gpt-5.6-luna", once_output.getvalue())
|
||||||
self.assertEqual(normalized_logs[0]["id"], 9007199254740997)
|
self.assertEqual(normalized_logs[0]["id"], 9007199254740997)
|
||||||
self.assertEqual(normalized_logs[0]["account_multiplier"], 0.003)
|
self.assertEqual(normalized_logs[0]["account_multiplier"], 0.003)
|
||||||
self.assertEqual(normalized_logs[0]["cost"], 0.25)
|
self.assertEqual(normalized_logs[0]["cost"], 0.25)
|
||||||
|
self.assertEqual(mod.account_rate_value({"account_rate_multiplier": 0.06}), 0.06)
|
||||||
|
self.assertEqual(
|
||||||
|
mod.account_rate_value(
|
||||||
|
{
|
||||||
|
"account_rate_multiplier": 0.06,
|
||||||
|
"account_rate_multiplier_cny": 0.003,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
0.003,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
mod.account_rate_value(
|
||||||
|
{
|
||||||
|
"account_rate_multiplier": 0.06,
|
||||||
|
"account_rate_multiplier_cny": "not-a-number",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
0.06,
|
||||||
|
)
|
||||||
|
self.assertEqual(mod.account_rate_value({"account_rate_multiplier_cny": 0}), 0)
|
||||||
|
self.assertIsNone(
|
||||||
|
mod.account_rate_value({"account_rate_multiplier": float("nan")})
|
||||||
|
)
|
||||||
|
self.assertEqual(mod.format_multiplier(0.003), "0.003x")
|
||||||
|
self.assertEqual(mod.format_multiplier(0), "0x")
|
||||||
|
self.assertEqual(mod.format_multiplier(float("inf")), "-")
|
||||||
self.assertEqual(mod.as_int("9007199254740999"), 9007199254740999)
|
self.assertEqual(mod.as_int("9007199254740999"), 9007199254740999)
|
||||||
self.assertEqual(len(keys["items"]), 1)
|
self.assertEqual(len(keys["items"]), 1)
|
||||||
self.assertEqual(mod.normalize_key_rows(keys)[0]["name"], "wmy")
|
self.assertEqual(mod.normalize_key_rows(keys)[0]["name"], "wmy")
|
||||||
@@ -863,6 +921,12 @@ class WorkspaceTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(RuntimeError, "invalid workspace requests"):
|
with self.assertRaisesRegex(RuntimeError, "invalid workspace requests"):
|
||||||
mod.fetch_workspace_payload("https://workspace.example.test/data", 3)
|
mod.fetch_workspace_payload("https://workspace.example.test/data", 3)
|
||||||
|
|
||||||
|
invalid_contract = workspace_payload_fixture()
|
||||||
|
invalid_contract["accounts"]["accounts"][0]["node_status"].pop("server4")
|
||||||
|
with mock.patch.object(mod, "fetch_workspace_response", return_value=(invalid_contract, "")):
|
||||||
|
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, "")):
|
||||||
@@ -1323,6 +1387,7 @@ class Sub2APILogsTests(unittest.TestCase):
|
|||||||
self.assertEqual(oldest["input_tokens"], 1200)
|
self.assertEqual(oldest["input_tokens"], 1200)
|
||||||
self.assertEqual(oldest["output_tokens"], 340)
|
self.assertEqual(oldest["output_tokens"], 340)
|
||||||
self.assertEqual(oldest["cache_tokens"], 50 + 4100)
|
self.assertEqual(oldest["cache_tokens"], 50 + 4100)
|
||||||
|
self.assertEqual(oldest["account_multiplier"], 0.003)
|
||||||
self.assertEqual(oldest["first_token_ms"], 800)
|
self.assertEqual(oldest["first_token_ms"], 800)
|
||||||
self.assertGreater(oldest["tokens_per_second"], 0)
|
self.assertGreater(oldest["tokens_per_second"], 0)
|
||||||
self.assertEqual(oldest["cost"], 0.0123)
|
self.assertEqual(oldest["cost"], 0.0123)
|
||||||
@@ -1399,6 +1464,11 @@ class Sub2APILogsTests(unittest.TestCase):
|
|||||||
self.assertIn("$0.012", text)
|
self.assertIn("$0.012", text)
|
||||||
self.assertIn("5.3s", text)
|
self.assertIn("5.3s", text)
|
||||||
self.assertIn("total 2.3K records", text)
|
self.assertIn("total 2.3K records", text)
|
||||||
|
lines = text.splitlines()
|
||||||
|
header = next(line for line in lines if line.startswith("node"))
|
||||||
|
multiplier_row = next(line for line in lines if "codex-main" in line)
|
||||||
|
self.assertEqual(header.index("key"), multiplier_row.index("codex-main"))
|
||||||
|
self.assertEqual(header.index("multiplier"), multiplier_row.index("0.003x"))
|
||||||
|
|
||||||
def test_default_logs_token_reads_config_file(self) -> None:
|
def test_default_logs_token_reads_config_file(self) -> None:
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
|
|||||||
Reference in New Issue
Block a user