112 lines
3.5 KiB
Python
112 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
|
|
from app.enums.credit_record import CreditRecordChargeKind
|
|
from app.enums.llm_billing import LlmBillingLedgerState
|
|
from app.services.generation.billing_service import build_credit_biz_key
|
|
|
|
|
|
class LlmBillingConfigurationError(RuntimeError):
|
|
"""LLM 场景积分配置无效。"""
|
|
|
|
|
|
class LlmBillingStateError(RuntimeError):
|
|
"""业务 attempt 的 LLM 积分消费状态不允许继续执行。"""
|
|
|
|
|
|
class LlmProviderPostprocessError(RuntimeError):
|
|
"""供应商请求已成功并返回用量,但响应内容或业务结构处理失败。"""
|
|
|
|
def __init__(self, message: str, *, usage: dict | None = None) -> None:
|
|
super().__init__(message)
|
|
self.usage = dict(usage or {})
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class LlmBillingPolicy:
|
|
charge_credits: float
|
|
valid: bool = True
|
|
error: str | None = None
|
|
policy_id: str | None = None
|
|
version: int | None = None
|
|
scene_name: str | None = None
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class LlmBillingContext:
|
|
user_id: str
|
|
owner_type: str
|
|
owner_id: str
|
|
attempt_no: int
|
|
charge_kind: str = CreditRecordChargeKind.TEXT_PROMPT.value
|
|
billing_scene: str | None = None
|
|
source_module: str | None = None
|
|
source_project_id: str | None = None
|
|
source_step_id: str | None = None
|
|
source_step_code: str | None = None
|
|
related_id: str | None = None
|
|
description_prefix: str = "LLM"
|
|
trace_id: str | None = None
|
|
request_id: str | None = None
|
|
celery_task_id: str | None = None
|
|
provider: str | None = None
|
|
model_name: str | None = None
|
|
model_config_id: str | None = None
|
|
model_parameters_snapshot: dict | None = None
|
|
token_usage_id: str | None = None
|
|
# 供应商响应成功事实先于审计落库设置。即使审计事务暂时失败,
|
|
# 后续异常处理也不能把真实供应商成功覆盖成 provider failed。
|
|
provider_call_succeeded: bool = False
|
|
provider_usage_snapshot: dict | None = None
|
|
request_time: datetime | None = None
|
|
billing_execution_id: str | None = None
|
|
current_call_attempt_id: str | None = None
|
|
|
|
@property
|
|
def charge_biz_key(self) -> str:
|
|
return build_credit_biz_key(
|
|
owner_type=self.owner_type,
|
|
owner_id=self.owner_id,
|
|
attempt_no=self.attempt_no,
|
|
charge_kind=self.charge_kind,
|
|
# 使用独立 llm_charge 幂等键,避免与历史 Token 按量计费的 :charge 流水碰撞;
|
|
# CreditRecord 的业务动作仍然记录为 charge(真实消费)。
|
|
action="llm_charge",
|
|
)
|
|
|
|
@property
|
|
def refund_biz_key(self) -> str:
|
|
return build_credit_biz_key(
|
|
owner_type=self.owner_type,
|
|
owner_id=self.owner_id,
|
|
attempt_no=self.attempt_no,
|
|
charge_kind=self.charge_kind,
|
|
action="refund",
|
|
)
|
|
|
|
@property
|
|
def billing_biz_key(self) -> str:
|
|
return self.charge_biz_key
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class LlmChargeResult:
|
|
amount: float
|
|
state: LlmBillingLedgerState
|
|
created: bool = False
|
|
record_id: str | None = None
|
|
reason: str | None = None
|
|
execution_id: str | None = None
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class LlmChargeValidation:
|
|
can_execute: bool
|
|
amount: float
|
|
state: LlmBillingLedgerState
|
|
reason: str | None = None
|
|
charge_record_id: str | None = None
|
|
execution_id: str | None = None
|