diff --git a/cost_control/ai_diag.py b/cost_control/ai_diag.py
index 950c8df..8641e07 100644
--- a/cost_control/ai_diag.py
+++ b/cost_control/ai_diag.py
@@ -267,6 +267,28 @@ async def _collect_diag_data(self) -> dict[str, Any]:
limits = self.get_budgets()
limits_cost = self.get_budgets_cost()
+ # 预算 cost 维度:限额与花费均换算到主货币口径(与 /budgets、/alerts 一致)
+ from .config import get_budgets_cost_currency
+ from .cost import compute_cost_grouped_in_main
+ from .exchange_rates import convert, get_main_currency, get_rates
+
+ _main_cur = get_main_currency(getattr(self, "cfg", None))
+ _rates = get_rates(getattr(self, "cfg", None))
+ _bcc = get_budgets_cost_currency(getattr(self, "cfg", None))
+ _pricing = self.get_pricing()
+ day_cost_c = compute_cost_grouped_in_main(
+ await self.query_usage_grouped(by="provider_model", start=d_start),
+ _pricing,
+ _main_cur,
+ _rates,
+ )
+ month_cost_c = compute_cost_grouped_in_main(
+ await self.query_usage_grouped(by="provider_model", start=m_start),
+ _pricing,
+ _main_cur,
+ _rates,
+ )
+
dims: list[dict[str, Any]] = []
dim_labels = {
"global_daily": "每日全局",
@@ -276,20 +298,32 @@ async def _collect_diag_data(self) -> dict[str, Any]:
"per_model_daily": "每模型·每日",
}
dim_used = {"global_daily": day_total, "global_monthly": month_total}
+ dim_used_c = {"global_daily": day_cost_c, "global_monthly": month_cost_c}
for d in _DIM_ORDER:
lt = int(limits.get(d, 0) or 0)
- lc = float(limits_cost.get(d, 0) or 0)
- used = dim_used.get(d, 0)
- if lt > 0 or lc > 0:
- ratio = round(used * 100.0 / lt, 1) if lt > 0 else 0
+ lc_raw = float(limits_cost.get(d, 0) or 0)
+ used_t = dim_used.get(d, 0)
+ used_c = dim_used_c.get(d, 0)
+ if lt > 0 or lc_raw > 0:
+ d_cur = str(_bcc.get(d, "") or "") or _main_cur
+ lc = (
+ round(convert(lc_raw, d_cur, _main_cur, _rates), 6)
+ if lc_raw > 0 and d_cur != _main_cur
+ else lc_raw
+ )
+ ratio_t = round(used_t * 100.0 / lt, 1) if lt > 0 else 0
+ ratio_c = round(used_c * 100.0 / lc, 1) if lc > 0 else 0
dims.append(
{
"dimension": dim_labels.get(d, d),
"token_limit": lt,
- "token_used": used,
- "token_ratio": ratio,
+ "token_used": used_t,
+ "token_ratio": ratio_t,
"cost_limit": lc,
- "exceeded": used >= lt if lt > 0 else False,
+ "cost_used": round(used_c, 6),
+ "cost_ratio": ratio_c,
+ "exceeded": (used_t >= lt if lt > 0 else False)
+ or (used_c >= lc if lc > 0 else False),
}
)
data["budgets"] = {"dimensions": dims}
diff --git a/cost_control/analytics.py b/cost_control/analytics.py
index 0b1bf92..8cf09a4 100644
--- a/cost_control/analytics.py
+++ b/cost_control/analytics.py
@@ -19,7 +19,7 @@
from .budget import day_window_start, resolve_tz
from .cache_diag import hit_rate
from .config import get_config
-from .cost import compute_cost_in_main, compute_row_cost, compute_row_cost_in_main
+from .cost import compute_cost_in_main, compute_row_cost_in_main
def report_window_start(
@@ -82,11 +82,6 @@ def compare_windows(
return cur_start, cur_end, prev_start, prev_end
-def _row_cost(row: dict[str, Any], pricing: dict[str, Any]) -> float:
- """按 (provider_id, model) 解析定价算单行成本(纯函数辅助)。无定价返回 0.0。"""
- return compute_row_cost(row, pricing)
-
-
def _row_cost_in_main(
row: dict[str, Any],
pricing: dict[str, Any],
diff --git a/cost_control/budget.py b/cost_control/budget.py
index cde19a4..1e14334 100644
--- a/cost_control/budget.py
+++ b/cost_control/budget.py
@@ -33,7 +33,6 @@
from .attributor import _str_tokens
from .config import enabled_overrides, get_config
-from .cost import compute_row_cost
def resolve_tz(context: Any) -> ZoneInfo:
@@ -132,24 +131,6 @@ def check_dimensions_dual(
return {"exceeded": False, "dim": None, "metric": None, "limit": 0.0, "used": 0.0}
-def _groups_cost(
- groups: list[dict[str, Any]],
- pricing: dict[str, Any],
-) -> float:
- """把 ``query_usage_grouped(by="provider_model")`` 结果按行求和花费(纯函数)。
-
- 每行含 ``provider_id`` / ``provider_model`` / ``count`` / token 字段,委托
- :func:`cost_control.cost.compute_row_cost`(按 provider_id 匹配用户定价、按 mode 计费)。
- """
- total = 0.0
- for g in groups or []:
- try:
- total += compute_row_cost(g, pricing)
- except Exception:
- continue
- return round(total, 6)
-
-
def total_tokens(usage: dict[str, Any]) -> int:
"""把三类 token 聚合为总数(纯函数)。"""
return (
@@ -327,7 +308,7 @@ async def _override_used(
provider_id: str | None,
d_start: datetime,
pricing: dict[str, Any],
- main_cur: str = "$",
+ main_cur: str = "USD",
rates: dict[str, float] | None = None,
) -> float:
"""按 override target 聚合当前周期的 ``metric`` 用量(token 数 / 主货币花费)。
@@ -679,7 +660,10 @@ def _format_message(self, result: dict[str, Any]) -> str:
cur = str(result.get("currency") or "") or get_main_currency(getattr(self, "cfg", None))
sym = currency_to_symbol(cur)
- return f"⏸ 已超出花费预算({dim}):{sym}{float(used or 0):.4f} / {sym}{float(limit or 0):.2f}"
+ return (
+ f"⏸ 已超出花费预算({dim}):{sym}{float(used or 0):.4f} / "
+ f"{sym}{float(limit or 0):.2f}"
+ )
# token 维度的 used/limit 来自 float() 包装,转 int 避免显示 "150.0"。
return f"⏸ 已超出预算({dim}):用 {int(used or 0)} / 限 {int(limit or 0)} token"
diff --git a/cost_control/commands.py b/cost_control/commands.py
index a22d55b..73b56e5 100644
--- a/cost_control/commands.py
+++ b/cost_control/commands.py
@@ -22,9 +22,9 @@
from .attributor import ESTIMATION_NOTE
from .budget import _DIM_ORDER, day_window_start, resolve_tz
-from .config import get_config
+from .config import get_budgets_cost_currency, get_config
from .cost import compute_row_cost_in_main
-from .exchange_rates import currency_to_symbol, get_main_currency, get_rates
+from .exchange_rates import convert, currency_to_symbol, get_main_currency, get_rates
# 插件主模块路径(``main.py``)。AstrBot 的 ``star_map`` 以 ``Main.__module__``
# 为键,而 ``update_command_permission`` 等管理接口通过 ``handler.__module__``
@@ -111,7 +111,10 @@ async def cmd_budget(self, event: AstrMessageEvent):
"""``/budget``:查询预算配置与当前超限状态。"""
try:
umo = self._umo(event)
- sym = currency_to_symbol(get_main_currency(getattr(self, "cfg", None)))
+ main_cur = get_main_currency(getattr(self, "cfg", None))
+ rates = get_rates(getattr(self, "cfg", None))
+ bcc = get_budgets_cost_currency(getattr(self, "cfg", None))
+ sym = currency_to_symbol(main_cur)
budgets = self.get_budgets()
budgets_cost = self.get_budgets_cost()
overrides = self.get_budget_overrides(getattr(self, "cfg", None))
@@ -126,7 +129,13 @@ async def cmd_budget(self, event: AstrMessageEvent):
if t > 0:
parts.append(f"token {t}")
if c > 0:
- parts.append(f"花费 {sym}{c:.2f}")
+ dim_cur = str(bcc.get(dim, "") or "") or main_cur
+ c_main = (
+ round(convert(c, dim_cur, main_cur, rates), 6)
+ if dim_cur != main_cur
+ else c
+ )
+ parts.append(f"花费 {sym}{c_main:.2f}")
lines.append(f" {dim}: " + " / ".join(parts))
any_cfg = True
if not any_cfg:
@@ -138,7 +147,14 @@ async def cmd_budget(self, event: AstrMessageEvent):
if ov.get("token_limit", 0) > 0:
parts.append(f"token {ov['token_limit']}")
if ov.get("cost_limit", 0) > 0:
- parts.append(f"花费 {sym}{ov['cost_limit']:.2f}")
+ ov_cur = str(ov.get("cost_currency") or "") or main_cur
+ ov_c = float(ov.get("cost_limit") or 0)
+ ov_c_main = (
+ round(convert(ov_c, ov_cur, main_cur, rates), 6)
+ if ov_cur != main_cur
+ else ov_c
+ )
+ parts.append(f"花费 {sym}{ov_c_main:.2f}")
lines.append(
f" · {ov.get('target_type')}:{ov.get('target_value')} "
f"({'/'.join(parts) or '不限'}) "
diff --git a/cost_control/cost.py b/cost_control/cost.py
index a024a36..1eb4ada 100644
--- a/cost_control/cost.py
+++ b/cost_control/cost.py
@@ -436,21 +436,3 @@ def get_rates(self) -> dict[str, float]:
from .config import get_rates
return get_rates(getattr(self, "cfg", None))
-
- async def compute_cost(
- self,
- usage: dict[str, Any],
- provider_id: str | None,
- model: str | None,
- ) -> float:
- """按生效定价把单条 usage 换算为 USD 成本。
-
- Args:
- usage: 聚合用量 dict。
- provider_id: Provider ID(用户定价匹配)。
- model: 模型名(默认表匹配)。
-
- Returns:
- USD 成本(float)。
- """
- return compute_cost_value(usage, provider_id, model, self.get_pricing())
diff --git a/cost_control/web_api.py b/cost_control/web_api.py
index c293bd6..cec4e3e 100644
--- a/cost_control/web_api.py
+++ b/cost_control/web_api.py
@@ -283,7 +283,7 @@ def _parse_iso(s: str | None, *, is_end: bool = False) -> datetime | None:
def _supplement_to_dict(
s: Any,
pricing: dict[str, Any] | None = None,
- main_cur: str = "$",
+ main_cur: str = "USD",
rates: dict[str, float] | None = None,
) -> dict[str, Any]:
"""把 ``CostSupplement`` 行序列化为 JSON 友好 dict。
@@ -499,6 +499,9 @@ async def api_alerts(self, **kwargs: Any) -> dict[str, Any]:
+ int(r.get("token_output", 0) or 0)
)
if unpriced_count > 0:
+ from .exchange_rates import currency_to_symbol, get_main_currency
+
+ _unpriced_sym = currency_to_symbol(get_main_currency(getattr(self, "cfg", None)))
alerts.append(
{
"level": "warn",
@@ -506,7 +509,8 @@ async def api_alerts(self, **kwargs: Any) -> dict[str, Any]:
"title": "存在未定价模型",
"detail": (
f"检测到 {unpriced_count} 个模型未配置定价"
- f"(涉及 {unpriced_tokens} token 用量),其成本被计为 $0,"
+ f"(涉及 {unpriced_tokens} token 用量),其成本被计为 "
+ f"{_unpriced_sym}0,"
"导致成本统计偏低。请前往定价页为对应 provider 设置单价。"
),
"tab": "pricing",
@@ -559,7 +563,8 @@ async def api_alerts(self, **kwargs: Any) -> dict[str, Any]:
# 将各维度 cost 限额换算到主货币(budgets_cost_currency 可能设了独立货币)
from .config import get_budgets_cost_currency
- from .exchange_rates import convert as _conv_alert, get_main_currency, get_rates
+ from .exchange_rates import convert as _conv_alert
+ from .exchange_rates import get_main_currency, get_rates
_alert_bcc = get_budgets_cost_currency(cfg)
_alert_main = get_main_currency(cfg)
@@ -641,7 +646,6 @@ async def api_compare(self, **kwargs: Any) -> dict[str, Any]:
from .analytics import compare_windows
from .budget import total_tokens
- from .cost import compute_cost_grouped
window = self._param("window", "daily") or "daily"
now = datetime.now(UTC)
@@ -804,8 +808,6 @@ async def api_records_aggregate(self, **kwargs: Any) -> dict[str, Any]:
(可选)、``start`` / ``end``(ISO)。返回每组的 token 三类、条数、成本、占比。
"""
try:
- from .cost import compute_row_cost
-
by = self._param("by", "model") or "model"
if by not in ("model", "provider", "umo"):
by = "model"
diff --git a/pages/dashboard/index.html b/pages/dashboard/index.html
index 9972385..e5346d7 100644
--- a/pages/dashboard/index.html
+++ b/pages/dashboard/index.html
@@ -13,6 +13,6 @@
-
+