from __future__ import annotations from dataclasses import dataclass 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 的账务流水状态不允许继续执行。""" @dataclass(slots=True, frozen=True) class LlmBillingPolicy: enabled: bool hold_credits: float config_key: str | None = None source_key: str | None = None valid: bool = True error: str | None = None @property def bypassed(self) -> bool: return not self.enabled @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 hold_credits: float | None = None hold_config_key: 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 token_usage_id: str | None = None @property def hold_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="hold", ) @property def hold_release_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="hold_release", ) @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, action="charge", ) @property def ledger_biz_keys(self) -> tuple[str, str, str]: return self.hold_biz_key, self.hold_release_biz_key, self.charge_biz_key @dataclass(slots=True, frozen=True) class LlmHoldResult: amount: float state: LlmBillingLedgerState created: bool = False record_id: str | None = None reason: str | None = None @property def bypassed(self) -> bool: return self.state == LlmBillingLedgerState.BILLING_BYPASSED @dataclass(slots=True, frozen=True) class LlmHoldValidation: can_execute: bool amount: float state: LlmBillingLedgerState reason: str | None = None hold_record_id: str | None = None @property def bypassed(self) -> bool: return self.state == LlmBillingLedgerState.BILLING_BYPASSED