mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
- 删除全部 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)
196 lines
5.9 KiB
Python
196 lines
5.9 KiB
Python
"""
|
||
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,
|
||
)
|
||
]
|