Files
Aether/_deprecated_py_src/services/billing/rule_defs/universal.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
2026-04-03 16:26:16 +08:00

196 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Universal billing template.
This is the single unified billing template for all task types.
Formula: total = (input_cost + output_cost + cache_creation_cost + cache_read_cost) + request_cost + video_cost
Each component can be 0 if not applicable for the specific task type.
"""
from __future__ import annotations
import re
from src.services.billing.default_rules import DefaultBillingRuleGenerator, VirtualBillingRule
from src.services.billing.rule_templates import CodeBillingRuleTemplate, RuleTemplateContext
def _get_nested(obj: object | None, path: str) -> object | None:
if not isinstance(obj, dict):
return None
cur: object = obj
for part in (path or "").split("."):
if not part:
continue
if not isinstance(cur, dict):
return None
cur = cur.get(part) # type: ignore[assignment]
return cur
def _as_float(v: object | None) -> float | None:
try:
if v is None:
return None
if isinstance(v, bool):
return None
return float(v)
except Exception:
return None
_WXH_PATTERN = re.compile(r"^(\d+)x(\d+)$")
def _normalize_resolution_key(raw: str) -> str:
"""
Normalize resolution key:
- lowercase, remove spaces, × → x
- For WxH format, sort dimensions so smaller comes first (1080x720 → 720x1080)
"""
k = (raw or "").strip().lower().replace(" ", "").replace("×", "x")
match = _WXH_PATTERN.match(k)
if match:
a, b = int(match.group(1)), int(match.group(2))
k = f"{a}x{b}" if a <= b else f"{b}x{a}"
return k
def _effective_unit_price(ctx: RuleTemplateContext) -> float:
"""Get video price per second from config."""
if ctx.model is not None:
v = _as_float(
_get_nested(getattr(ctx.model, "config", None), "billing.video.price_per_second")
)
if v is not None:
return v
v = _as_float(
_get_nested(getattr(ctx.global_model, "config", None), "billing.video.price_per_second")
)
if v is not None:
return v
return 0.0
def _effective_resolution_price_per_second(ctx: RuleTemplateContext) -> dict[str, float]:
"""
Resolution (or size) -> price_per_second.
"""
for conf in (
getattr(ctx.model, "config", None) if ctx.model is not None else None,
getattr(ctx.global_model, "config", None),
):
raw = _get_nested(conf, "billing.video.price_per_second_by_resolution")
if not isinstance(raw, dict):
continue
out: dict[str, float] = {}
for k, v in raw.items():
fk = _normalize_resolution_key(str(k))
fv = _as_float(v)
if not fk:
continue
if fv is None:
continue
out[fk] = fv
if out:
return out
# Backward-compat: resolution multipliers
base = _effective_unit_price(ctx)
if base and base > 0:
for conf in (
getattr(ctx.model, "config", None) if ctx.model is not None else None,
getattr(ctx.global_model, "config", None),
):
raw = _get_nested(conf, "billing.video.resolution_multipliers")
if not isinstance(raw, dict):
continue
out2: dict[str, float] = {}
for k, v in raw.items():
fk = _normalize_resolution_key(str(k))
mv = _as_float(v)
if not fk:
continue
if mv is None:
continue
out2[fk] = float(base) * float(mv)
if out2:
return out2
return {}
def build_universal(ctx: RuleTemplateContext) -> VirtualBillingRule:
"""
Build the universal billing rule.
Formula:
total = (input_cost + output_cost + cache_creation_cost + cache_read_cost) + request_cost + video_cost
Each component defaults to 0 if not configured or not applicable.
"""
# Base rule: token + per-request
base = DefaultBillingRuleGenerator.generate_for_model(
global_model=ctx.global_model,
model=ctx.model,
task_type=ctx.task_type,
)
unit_price = _effective_unit_price(ctx)
resolution_price_map = _effective_resolution_price_per_second(ctx)
variables = dict(base.variables or {})
dimension_mappings = dict(base.dimension_mappings or {})
# Video duration dimension
dimension_mappings["duration_seconds"] = {
"source": "dimension",
"key": "duration_seconds",
"required": False,
"allow_zero": True,
"default": 0,
}
# Video price per second (resolved from resolution map or fallback to unit price)
dimension_mappings["video_price_per_second"] = {
"source": "matrix",
"key": "video_resolution_key",
"required": False,
"default": unit_price,
"map": resolution_price_map,
}
# Video cost component
dimension_mappings["video_cost"] = {
"source": "computed",
"required": False,
"default": 0,
"expression": "duration_seconds * video_price_per_second",
}
# Universal formula: token costs + request cost + video cost
# base.expression = "input_cost + output_cost + cache_creation_cost + cache_read_cost + request_cost"
expression = f"({base.expression}) + video_cost"
return VirtualBillingRule(
id="__default__",
name="Universal Billing Rule",
task_type=ctx.task_type,
expression=expression,
variables=variables,
dimension_mappings=dimension_mappings,
is_virtual=True,
)
TEMPLATES = [
CodeBillingRuleTemplate(
name="universal",
description="Universal billing: (input + output + cache) + request + video. All components default to 0 if not applicable.",
task_types={"chat", "cli", "video", "image", "audio"},
priority=100, # Highest priority - used for all task types
build=build_universal,
)
]